[ABANDONED] API server for Flexor social network.
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.
 

393 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.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.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
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' },
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,
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,
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
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' },
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,
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,
about,
websiteUrl,
companyName,
composerUrl,
composerSchema,
rendererUrl,
rendererSchema,
initCallbackUrl,
composeCallbackUrl,
revisions: [
...app.revisions,
{
version: app.version,
name: app.name,
imageUrl: app.imageUrl,
coverImageUrl: app.coverImageUrl,
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