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