Dwayne Harris
5 years ago
11 changed files with 454 additions and 32 deletions
-
2src/constants.ts
-
3src/lib/crypto.ts
-
9src/lib/database.ts
-
11src/lib/utils.ts
-
382src/plugins/api/apps.ts
-
4src/plugins/api/authentication.ts
-
2src/plugins/api/groups.ts
-
2src/plugins/api/posts.ts
-
4src/plugins/api/users.ts
-
41src/schemas.ts
-
26src/types/collections.ts
@ -0,0 +1,382 @@ |
|||||
|
import { |
||||
|
FastifyInstance, |
||||
|
Plugin, |
||||
|
DefaultQuery, |
||||
|
DefaultParams, |
||||
|
DefaultHeaders, |
||||
|
DefaultBody, |
||||
|
RouteShorthandOptions, |
||||
|
} from 'fastify' |
||||
|
|
||||
|
import { Server, IncomingMessage, ServerResponse } from 'http' |
||||
|
|
||||
|
import { appSchema, errorSchema } from '../../schemas' |
||||
|
import { generateString } from '../../lib/crypto' |
||||
|
import { containerFor, getItem, normalize } from '../../lib/database' |
||||
|
import { unauthorizedError, serverError, badRequestError, notFoundError } from '../../lib/errors' |
||||
|
|
||||
|
import { APP_PARTITION_KEY } from '../../constants' |
||||
|
|
||||
|
import { |
||||
|
App, |
||||
|
AppRevision, |
||||
|
} from '../../types/collections' |
||||
|
|
||||
|
import { PluginOptions } from '../../types' |
||||
|
|
||||
|
function appsRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { |
||||
|
interface Query { |
||||
|
sort?: string |
||||
|
continuation?: string |
||||
|
} |
||||
|
|
||||
|
const options: RouteShorthandOptions = { |
||||
|
schema: { |
||||
|
querystring: { |
||||
|
type: 'object', |
||||
|
properties: { |
||||
|
sort: { type: 'string' }, |
||||
|
continuation: { type: 'string' }, |
||||
|
}, |
||||
|
}, |
||||
|
response: { |
||||
|
200: { |
||||
|
type: 'object', |
||||
|
properties: { |
||||
|
apps: { |
||||
|
type: 'array', |
||||
|
items: appSchema, |
||||
|
}, |
||||
|
continuation: { type: 'string' }, |
||||
|
}, |
||||
|
}, |
||||
|
400: errorSchema, |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
server.get<Query, DefaultParams, DefaultHeaders, DefaultBody>('/api/apps', options, async (request, reply) => { |
||||
|
if (!server.database) return serverError(reply) |
||||
|
|
||||
|
const { sort = 'created', continuation } = request.query |
||||
|
const container = containerFor(server.database.client, 'Apps') |
||||
|
|
||||
|
const { resources: apps, requestCharge, continuation: newContinuation } = await container.items.query<App>( |
||||
|
`SELECT
|
||||
|
a.id, |
||||
|
a.name, |
||||
|
a.rating, |
||||
|
a.created, |
||||
|
r.displayName, |
||||
|
r.imageUrl, |
||||
|
r.coverImageUrl, |
||||
|
r.about, |
||||
|
r.websiteUrl, |
||||
|
r.companyName |
||||
|
FROM Apps a JOIN r IN a.revisions[a.currentRevisionIndex] |
||||
|
WHERE a.pk = '${APP_PARTITION_KEY}' AND a.active = true ORDER BY a.${sort}`,
|
||||
|
{ |
||||
|
maxItemCount: 40, |
||||
|
continuation, |
||||
|
} |
||||
|
).fetchAll() |
||||
|
|
||||
|
request.log.trace('Query: %d', requestCharge) |
||||
|
|
||||
|
return { |
||||
|
apps, |
||||
|
continuation: newContinuation, |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function selfAppsRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { |
||||
|
interface Query { |
||||
|
sort?: string |
||||
|
} |
||||
|
|
||||
|
const options: RouteShorthandOptions = { |
||||
|
schema: { |
||||
|
querystring: { |
||||
|
type: 'object', |
||||
|
properties: { |
||||
|
sort: { type: 'string' }, |
||||
|
}, |
||||
|
}, |
||||
|
response: { |
||||
|
200: { |
||||
|
type: 'object', |
||||
|
properties: { |
||||
|
apps: { |
||||
|
type: 'array', |
||||
|
items: appSchema, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
400: errorSchema, |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
server.get<Query, DefaultParams, DefaultHeaders, DefaultBody>('/api/self/apps', options, async (request, reply) => { |
||||
|
if (!server.database) return serverError(reply) |
||||
|
if (!request.viewer) return unauthorizedError(reply) |
||||
|
|
||||
|
const { sort = 'created' } = request.query |
||||
|
const container = containerFor(server.database.client, 'Apps') |
||||
|
|
||||
|
const { resources: apps, requestCharge, continuation: newContinuation } = await container.items.query<App>( |
||||
|
`SELECT
|
||||
|
a.id, |
||||
|
a.name, |
||||
|
a.rating, |
||||
|
a.created, |
||||
|
r.displayName, |
||||
|
r.imageUrl, |
||||
|
r.coverImageUrl, |
||||
|
r.about, |
||||
|
r.websiteUrl, |
||||
|
r.companyName |
||||
|
FROM Apps a JOIN r IN a.revisions[a.currentRevisionIndex] |
||||
|
WHERE a.pk = '${APP_PARTITION_KEY}' AND a.userId = ${request.viewer.id} ORDER BY a.${sort}`,
|
||||
|
{} |
||||
|
).fetchAll() |
||||
|
|
||||
|
request.log.trace('Query: %d', requestCharge) |
||||
|
|
||||
|
return { |
||||
|
apps, |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { |
||||
|
interface Body { |
||||
|
name: string |
||||
|
displayName: string |
||||
|
version: string |
||||
|
imageUrl?: string |
||||
|
coverImageUrl?: string |
||||
|
about?: string |
||||
|
websiteUrl?: string |
||||
|
companyName?: string |
||||
|
composerUrl?: string |
||||
|
composerSchema?: object |
||||
|
rendererUrl?: string |
||||
|
rendererSchema?: object |
||||
|
initCallbackUrl: string |
||||
|
composeCallbackUrl: string |
||||
|
} |
||||
|
|
||||
|
const options: RouteShorthandOptions = { |
||||
|
schema: { |
||||
|
body: { |
||||
|
type: 'object', |
||||
|
required: ['name', 'displayName', 'version'], |
||||
|
properties: { |
||||
|
name: { type: 'string' }, |
||||
|
displayName: { type: 'string' }, |
||||
|
version: { type: 'string' }, |
||||
|
imageUrl: { type: 'string' }, |
||||
|
coverImageUrl: { type: 'string' }, |
||||
|
about: { type: 'string' }, |
||||
|
websiteUrl: { type: 'string' }, |
||||
|
companyName: { type: 'string' }, |
||||
|
composerUrl: { type: 'string' }, |
||||
|
composerSchema: { type: 'object' }, |
||||
|
rendererUrl: { type: 'string' }, |
||||
|
rendererSchema: { type: 'object' }, |
||||
|
initCallbackUrl: { type: 'string' }, |
||||
|
composeCallbackUrl: { type: 'string' }, |
||||
|
}, |
||||
|
}, |
||||
|
response: { |
||||
|
200: { |
||||
|
type: 'object', |
||||
|
properties: { |
||||
|
id: { type: 'string' }, |
||||
|
}, |
||||
|
}, |
||||
|
400: errorSchema, |
||||
|
} |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
server.post<DefaultQuery, DefaultParams, DefaultHeaders, Body>('/api/app', options, async (request, reply) => { |
||||
|
if (!server.database) return serverError(reply) |
||||
|
if (!request.viewer) return unauthorizedError(reply) |
||||
|
|
||||
|
const container = containerFor(server.database.client, 'Apps') |
||||
|
const id = normalize(request.body.name) |
||||
|
|
||||
|
const { |
||||
|
name, |
||||
|
displayName, |
||||
|
version, |
||||
|
imageUrl, |
||||
|
coverImageUrl, |
||||
|
about, |
||||
|
websiteUrl, |
||||
|
companyName, |
||||
|
composerUrl, |
||||
|
composerSchema, |
||||
|
rendererUrl, |
||||
|
rendererSchema, |
||||
|
initCallbackUrl, |
||||
|
composeCallbackUrl, |
||||
|
} = request.body |
||||
|
|
||||
|
const existingApp = await getItem<App>({ container, id }) |
||||
|
if (existingApp) return badRequestError(reply, 'Name already used') |
||||
|
|
||||
|
const revision: AppRevision = { |
||||
|
version, |
||||
|
displayName, |
||||
|
imageUrl, |
||||
|
coverImageUrl, |
||||
|
about, |
||||
|
websiteUrl, |
||||
|
companyName, |
||||
|
composerUrl, |
||||
|
composerSchema, |
||||
|
rendererUrl, |
||||
|
rendererSchema, |
||||
|
initCallbackUrl, |
||||
|
composeCallbackUrl, |
||||
|
} |
||||
|
|
||||
|
await container.items.create<App>({ |
||||
|
id, |
||||
|
pk: APP_PARTITION_KEY, |
||||
|
userId: request.viewer.id, |
||||
|
name, |
||||
|
version, |
||||
|
rating: 0, |
||||
|
revisions: [revision], |
||||
|
currentRevisionIndex: 0, |
||||
|
publicKey: generateString(20), |
||||
|
privateKey: generateString(40), |
||||
|
active: false, |
||||
|
created: Date.now(), |
||||
|
}) |
||||
|
|
||||
|
return { |
||||
|
id, |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) { |
||||
|
interface Params { |
||||
|
id: string |
||||
|
} |
||||
|
|
||||
|
interface Body { |
||||
|
version: string |
||||
|
displayName?: string |
||||
|
imageUrl?: string |
||||
|
coverImageUrl?: string |
||||
|
about?: string |
||||
|
websiteUrl?: string |
||||
|
companyName?: string |
||||
|
composerUrl?: string |
||||
|
composerSchema?: object |
||||
|
rendererUrl?: string |
||||
|
rendererSchema?: object |
||||
|
initCallbackUrl: string |
||||
|
composeCallbackUrl: string |
||||
|
} |
||||
|
|
||||
|
const options: RouteShorthandOptions = { |
||||
|
schema: { |
||||
|
body: { |
||||
|
type: 'object', |
||||
|
required: ['version'], |
||||
|
properties: { |
||||
|
version: { type: 'string' }, |
||||
|
displayName: { type: 'string' }, |
||||
|
imageUrl: { type: 'string' }, |
||||
|
coverImageUrl: { type: 'string' }, |
||||
|
about: { type: 'string' }, |
||||
|
websiteUrl: { type: 'string' }, |
||||
|
companyName: { type: 'string' }, |
||||
|
composerUrl: { type: 'string' }, |
||||
|
composerSchema: { type: 'object' }, |
||||
|
rendererUrl: { type: 'string' }, |
||||
|
rendererSchema: { type: 'object' }, |
||||
|
initCallbackUrl: { type: 'string' }, |
||||
|
composeCallbackUrl: { type: 'string' }, |
||||
|
}, |
||||
|
}, |
||||
|
response: { |
||||
|
400: errorSchema, |
||||
|
} |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
server.put<DefaultQuery, Params, DefaultHeaders, Body>('/api/app/:id', options, async (request, reply) => { |
||||
|
if (!server.database) return serverError(reply) |
||||
|
if (!request.viewer) return unauthorizedError(reply) |
||||
|
|
||||
|
const container = containerFor(server.database.client, 'Apps') |
||||
|
const id = request.params.id |
||||
|
|
||||
|
const { |
||||
|
version, |
||||
|
displayName, |
||||
|
imageUrl, |
||||
|
coverImageUrl, |
||||
|
about, |
||||
|
websiteUrl, |
||||
|
companyName, |
||||
|
composerUrl, |
||||
|
composerSchema, |
||||
|
rendererUrl, |
||||
|
rendererSchema, |
||||
|
initCallbackUrl, |
||||
|
composeCallbackUrl, |
||||
|
} = request.body |
||||
|
|
||||
|
const appItem = container.item(request.params.id, APP_PARTITION_KEY) |
||||
|
const { resource: app } = await appItem.read<App>() |
||||
|
if (!app) return notFoundError(reply) |
||||
|
|
||||
|
const revision = app.revisions[app.revisions.length - 1] |
||||
|
|
||||
|
await appItem.replace<App>({ |
||||
|
...app, |
||||
|
revisions: [ |
||||
|
...app.revisions, |
||||
|
{ |
||||
|
...revision, |
||||
|
version, |
||||
|
displayName: displayName || revision.displayName, |
||||
|
imageUrl, |
||||
|
coverImageUrl, |
||||
|
about, |
||||
|
websiteUrl, |
||||
|
companyName, |
||||
|
composerUrl, |
||||
|
composerSchema, |
||||
|
rendererUrl, |
||||
|
rendererSchema, |
||||
|
initCallbackUrl, |
||||
|
composeCallbackUrl, |
||||
|
} |
||||
|
], |
||||
|
currentRevisionIndex: app.currentRevisionIndex + 1, |
||||
|
version, |
||||
|
}) |
||||
|
|
||||
|
reply.code(201) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => { |
||||
|
appsRoute(server) |
||||
|
selfAppsRoute(server) |
||||
|
createRoute(server) |
||||
|
updateRoute(server) |
||||
|
} |
||||
|
|
||||
|
export default plugin |
Write
Preview
Loading…
Cancel
Save
Reference in new issue