You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
404 lines
12 KiB
404 lines
12 KiB
import {
|
|
FastifyInstance,
|
|
Plugin,
|
|
DefaultQuery,
|
|
DefaultParams,
|
|
DefaultHeaders,
|
|
DefaultBody,
|
|
RouteShorthandOptions,
|
|
} from 'fastify'
|
|
|
|
import { Server, IncomingMessage, ServerResponse } from 'http'
|
|
|
|
import { appSchema, errorSchema } from '../../schemas'
|
|
import { getUsers } from '../../lib/collections'
|
|
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, User } 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.userId,
|
|
a.version,
|
|
a.name,
|
|
a.imageUrl,
|
|
a.coverImageUrl,
|
|
a.iconImageUrl,
|
|
a.about,
|
|
a.websiteUrl,
|
|
a.companyName
|
|
a.rating,
|
|
a.created
|
|
FROM Apps a
|
|
WHERE a.pk = '${APP_PARTITION_KEY}' AND a.active = true ORDER BY a.${sort}`,
|
|
{
|
|
maxItemCount: 40,
|
|
continuation,
|
|
}
|
|
).fetchAll()
|
|
|
|
request.log.trace('Query: %d', requestCharge)
|
|
|
|
const users = await getUsers(server.database.client, apps.map(app => app.userId), request.log)
|
|
|
|
return {
|
|
apps: apps.map(app => ({
|
|
...app,
|
|
user: users.find(user => user.id === app.userId),
|
|
userId: undefined,
|
|
})),
|
|
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 viewer = containerFor(server.database.client, 'Users').item(request.viewer.id, request.viewer.id).read<User>()
|
|
|
|
const { sort = 'created' } = request.query
|
|
const container = containerFor(server.database.client, 'Apps')
|
|
|
|
const { resources: apps, requestCharge } = await container.items.query<App>(
|
|
`SELECT
|
|
a.id,
|
|
a.version,
|
|
a.name,
|
|
a.imageUrl,
|
|
a.coverImageUrl,
|
|
a.iconImageUrl,
|
|
a.about,
|
|
a.websiteUrl,
|
|
a.companyName
|
|
a.rating,
|
|
a.created
|
|
FROM Apps a
|
|
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: apps.map(app => ({
|
|
...app,
|
|
user: viewer,
|
|
userId: undefined,
|
|
})),
|
|
}
|
|
})
|
|
}
|
|
|
|
function createRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
|
|
interface Body {
|
|
version: string
|
|
name: string
|
|
imageUrl?: string
|
|
coverImageUrl?: string
|
|
iconImageUrl?: 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', 'name'],
|
|
properties: {
|
|
version: { type: 'string' },
|
|
name: { type: 'string' },
|
|
imageUrl: { type: 'string' },
|
|
coverImageUrl: { type: 'string' },
|
|
iconImageUrl: { 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 {
|
|
version,
|
|
name,
|
|
imageUrl,
|
|
coverImageUrl,
|
|
iconImageUrl,
|
|
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')
|
|
|
|
await container.items.create<App>({
|
|
id,
|
|
pk: APP_PARTITION_KEY,
|
|
userId: request.viewer.id,
|
|
version,
|
|
name,
|
|
imageUrl,
|
|
coverImageUrl,
|
|
iconImageUrl,
|
|
about,
|
|
websiteUrl,
|
|
companyName,
|
|
composerUrl,
|
|
composerSchema,
|
|
rendererUrl,
|
|
rendererSchema,
|
|
initCallbackUrl,
|
|
composeCallbackUrl,
|
|
rating: 0,
|
|
publicKey: generateString(20),
|
|
privateKey: generateString(40),
|
|
preinstall: false,
|
|
revisions: [],
|
|
active: false,
|
|
updated: Date.now(),
|
|
created: Date.now(),
|
|
})
|
|
|
|
return {
|
|
id,
|
|
}
|
|
})
|
|
}
|
|
|
|
function updateRoute(server: FastifyInstance<Server, IncomingMessage, ServerResponse>) {
|
|
interface Params {
|
|
id: string
|
|
}
|
|
|
|
interface Body {
|
|
version: string
|
|
name: string
|
|
imageUrl?: string
|
|
coverImageUrl?: string
|
|
iconImageUrl?: 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', 'name'],
|
|
properties: {
|
|
version: { type: 'string' },
|
|
imageUrl: { type: 'string' },
|
|
coverImageUrl: { type: 'string' },
|
|
iconImageUrl: { 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 {
|
|
version,
|
|
name,
|
|
imageUrl,
|
|
coverImageUrl,
|
|
iconImageUrl,
|
|
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)
|
|
|
|
await appItem.replace<App>({
|
|
...app,
|
|
version,
|
|
name: name || app.name,
|
|
imageUrl,
|
|
coverImageUrl,
|
|
iconImageUrl,
|
|
about,
|
|
websiteUrl,
|
|
companyName,
|
|
composerUrl,
|
|
composerSchema,
|
|
rendererUrl,
|
|
rendererSchema,
|
|
initCallbackUrl,
|
|
composeCallbackUrl,
|
|
revisions: [
|
|
...app.revisions,
|
|
{
|
|
version: app.version,
|
|
name: app.name,
|
|
imageUrl: app.imageUrl,
|
|
coverImageUrl: app.coverImageUrl,
|
|
iconImageUrl: app.iconImageUrl,
|
|
about: app.about,
|
|
websiteUrl: app.websiteUrl,
|
|
companyName: app.companyName,
|
|
composerUrl: app.composerUrl,
|
|
composerSchema: app.composerSchema,
|
|
rendererUrl: app.rendererUrl,
|
|
rendererSchema: app.rendererSchema,
|
|
initCallbackUrl: app.initCallbackUrl,
|
|
composeCallbackUrl: app.composeCallbackUrl,
|
|
created: app.updated,
|
|
}
|
|
],
|
|
updated: Date.now(),
|
|
})
|
|
|
|
reply.code(201)
|
|
})
|
|
}
|
|
|
|
const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => {
|
|
appsRoute(server)
|
|
selfAppsRoute(server)
|
|
createRoute(server)
|
|
updateRoute(server)
|
|
}
|
|
|
|
export default plugin
|