Dwayne Harris 5 years ago
parent
commit
3341a86720
  1. 6
      package-lock.json
  2. 2
      package.json
  3. 21
      src/lib/collections.ts
  4. 4
      src/lib/database.ts
  5. 10
      src/lib/errors.ts
  6. 13
      src/plugins/api/authentication.ts
  7. 193
      src/plugins/api/posts.ts
  8. 2
      src/plugins/api/profile.ts
  9. 1
      src/types/collections.ts

6
package-lock.json

@ -63,6 +63,12 @@
"@types/node": "*"
}
},
"@types/lodash": {
"version": "4.14.137",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.137.tgz",
"integrity": "sha512-g4rNK5SRKloO+sUGbuO7aPtwbwzMgjK+bm9BBhLD7jGUiGR7zhwYEhSln/ihgYQBeIJ5j7xjyaYzrWTcu3UotQ==",
"dev": true
},
"@types/node": {
"version": "12.7.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.0.tgz",

2
package.json

@ -13,6 +13,7 @@
"devDependencies": {
"@types/dotenv": "^6.1.1",
"@types/jsonwebtoken": "^8.3.2",
"@types/lodash": "^4.14.137",
"@types/uuid": "^3.4.5",
"nodemon": "^1.19.1",
"npm-run-all": "^4.1.5",
@ -26,6 +27,7 @@
"fastify": "^2.7.1",
"fastify-helmet": "^3.0.1",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.15",
"uuid": "^3.3.2",
"winston": "^3.2.1"
}

21
src/lib/collections.ts

@ -0,0 +1,21 @@
import { CosmosClient } from '@azure/cosmos'
import uniq from 'lodash/uniq'
import { containerFor, IDatabaseItem } from './database'
import { IUser } from '../types/collections'
export async function getUsers(client: CosmosClient, ids: string[]): Promise<IUser[]> {
const container = await containerFor(client, 'Users')
const { resources: users } = await container.items.query<IUser>({
query: 'SELECT u.id, u.name, u.created FROM Users u WHERE ARRAY_CONTAINS(@ids, u.id)',
parameters: [{
name: '@ids',
value: uniq(ids),
}],
}, {}).fetchAll()
return users
}
export async function getUsersFromItems<T extends IDatabaseItem>(client: CosmosClient, items: T[]): Promise<IUser[]> {
return await getUsers(client, items.map(i => i.id))
}

4
src/lib/database.ts

@ -4,6 +4,10 @@ interface IQueryParams {
[key: string]: string
}
export interface IDatabaseItem {
id: string
}
export async function containerFor(client: CosmosClient, containerId: string): Promise<Container> {
const { database } = await client.databases.createIfNotExists({ id: 'Flexor' })
const { container } = await database.containers.createIfNotExists({ id: containerId })

10
src/lib/errors.ts

@ -37,6 +37,16 @@ export function unauthorizedError(reply: FastifyReply<ServerResponse>): IHttpErr
}
}
export function notFoundError(reply: FastifyReply<ServerResponse>): IHttpError {
const statusCode = 404
reply.code(statusCode)
return {
statusCode,
error: 'Not found'
}
}
export function serverError(reply: FastifyReply<ServerResponse>, error: string = 'Server Error'): IHttpError {
const statusCode = 500
reply.code(statusCode)

13
src/plugins/api/authentication.ts

@ -13,7 +13,7 @@ import { Server, IncomingMessage, ServerResponse } from 'http'
import { createAccessToken, createRefreshToken } from '../../lib/authentication'
import { hashPassword, verifyPassword, JWT } from '../../lib/crypto'
import { containerFor, createQuerySpec, normalize } from '../../lib/database'
import { containerFor, normalize } from '../../lib/database'
import { errorSchema, badRequestError, serverError, unauthorizedError } from '../../lib/errors'
import { tokenFromHeader } from '../../lib/http'
@ -74,8 +74,7 @@ function registerRoute(server: FastifyInstance<Server, IncomingMessage, ServerRe
if (!server.database) return serverError(reply, 'Database error')
const container = await containerFor(server.database.client, 'Users')
const existingUserItem = await container.item(id, id)
const { resource: existingUser } = await existingUserItem.read<IUser>()
const { resource: existingUser } = await container.item(id, id).read<IUser>()
if (existingUser) return badRequestError(reply, 'User id already taken', 'id')
@ -137,8 +136,7 @@ function authenticateRoute(server: FastifyInstance<Server, IncomingMessage, Serv
const { password } = request.body
const id = normalize(request.body.id)
const container = await containerFor(server.database.client, 'Users')
const userItem = container.item(id, id)
const { resource: user } = await userItem.read<IUser>()
const { resource: user } = await container.item(id, id).read<IUser>()
if (!user) return badRequestError(reply, 'User not found')
@ -207,7 +205,7 @@ function refreshRoute(server: FastifyInstance<Server, IncomingMessage, ServerRes
}
const container = await containerFor(server.database.client, 'Users')
const tokenItem = await container.item(request.body.refresh, userId)
const tokenItem = container.item(request.body.refresh, userId)
if (!tokenItem) return badRequestError(reply, 'Invalid refresh token')
const { resource: token } = await tokenItem.read<IUserToken>()
@ -246,8 +244,7 @@ function selfRoute(server: FastifyInstance<Server, IncomingMessage, ServerRespon
if (!request.user) return unauthorizedError(reply)
const container = await containerFor(server.database.client, 'Users')
const userItem = await container.item(request.user.id, request.user.id)
const { resource: user } = await userItem.read<IUser>()
const { resource: user } = await container.item(request.user.id, request.user.id).read<IUser>()
if (!user) return unauthorizedError(reply)

193
src/plugins/api/posts.ts

@ -3,20 +3,59 @@ import fastify, {
DefaultQuery,
DefaultParams,
DefaultHeaders,
DefaultBody,
RouteShorthandOptions,
JSONSchema,
} from 'fastify'
import { Server, IncomingMessage, ServerResponse } from 'http'
import { unauthorizedError, badRequestError } from '../../lib/errors'
import { containerFor, createQuerySpec } from '../../lib/database'
import { unauthorizedError, badRequestError, notFoundError } from '../../lib/errors'
import { trimContent, createPostId } from '../../lib/util'
import { IPost, IPostAttachment, IUserPost, IUserFollow, IStatus, IUserTimelinePost, IPostRelationship } from '../../types/collections'
import { getUsers } from '../../lib/collections'
import {
containerFor,
createQuerySpec,
normalize,
} from '../../lib/database'
import {
IPost,
IPostAttachment,
IUser,
IUserPost,
IUserFollow,
IUserTimelinePost,
IPostRelationship,
IStatus,
} from '../../types/collections'
interface PluginOptions {
}
function postRoute(server: fastify.FastifyInstance<Server, IncomingMessage, ServerResponse>) {
const postSchema: JSONSchema = {
type: 'object',
properties: {
id: { type: 'string' },
userId: { type: 'string' },
text: { type: 'string' },
cover: { type: 'string' },
visible: { type: 'boolean' },
created: { type: 'number' },
},
}
const userSchema: JSONSchema = {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
created: { type: 'number' },
},
}
function doPostRoute(server: fastify.FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Body {
text?: string
cover?: string
@ -82,13 +121,10 @@ function postRoute(server: fastify.FastifyInstance<Server, IncomingMessage, Serv
const postId = createPostId()
if (request.body.parent) {
const parentItem = postContainer.item(request.body.parent, request.body.parent)
const { resource: parent } = await parentItem.read<IPost>()
const { resource: parent } = await postContainer.item(request.body.parent, request.body.parent).read<IPost>()
if (!parent) return badRequestError(reply, 'Invalid parent', 'parent')
const parentRelationshipItem = postRelationshipContainer.item(request.body.parent, parent.root)
const { resource: parentRelationship } = await parentRelationshipItem.read<IPostRelationship>()
const { resource: parentRelationship } = await postRelationshipContainer.item(request.body.parent, parent.root).read<IPostRelationship>()
const parents = parentRelationship ? parentRelationship.parents : []
newPostRelationship = {
@ -105,6 +141,7 @@ function postRoute(server: fastify.FastifyInstance<Server, IncomingMessage, Serv
id: postId,
userId: request.user.id,
root: newPostRelationship ? newPostRelationship.partitionKey : postId,
parents: newPostRelationship ? newPostRelationship.parents : [],
text: trimContent(request.body.text, 1000),
cover: trimContent(request.body.cover),
visible: request.body.visible,
@ -145,7 +182,145 @@ function postRoute(server: fastify.FastifyInstance<Server, IncomingMessage, Serv
})
}
function postsByUserRoute(server: fastify.FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Params {
id: string
}
const options: RouteShorthandOptions = {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
response: {
200: {
type: 'object',
properties: {
user: userSchema,
posts: {
type: 'array',
items: postSchema,
},
},
},
},
},
}
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/user/:id/posts', options, async (request, reply) => {
const id = normalize(request.params.id)
const userContainer = await containerFor(server.database.client, 'Users')
const postContainer = await containerFor(server.database.client, 'Posts')
const { resource: user } = await userContainer.item(id, id).read<IUser>()
if (!user) return notFoundError(reply)
const userPostsQuery = createQuerySpec(`SELECT p.id FROM Users p WHERE p.partitionKey = @userId AND p.type = 'post'`, { userId: id })
const { resources: userPosts } = await userContainer.items.query<IUserPost>(userPostsQuery, {}).fetchAll()
const { resources: posts } = await postContainer.items.query<IPost>({
query: 'SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@posts, p.id)',
parameters: [{
name: '@posts',
value: userPosts.map(p => p.id),
}]
}, {}).fetchAll()
return {
user,
posts,
}
})
}
function postRoute(server: fastify.FastifyInstance<Server, IncomingMessage, ServerResponse>) {
interface Params {
id: string
}
const options: RouteShorthandOptions = {
schema: {
params: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
response: {
200: {
type: 'object',
properties: {
post: postSchema,
descendants: {
type: 'array',
items: postSchema,
},
ancestors: {
type: 'array',
items: postSchema,
},
users: {
type: 'array',
items: userSchema,
}
},
},
},
},
}
server.get<DefaultQuery, Params, DefaultHeaders, DefaultBody>('/api/post/:id', options, async (request, reply) => {
const postContainer = await containerFor(server.database.client, 'Posts')
const { resource: post } = await postContainer.item(request.params.id, request.params.id).read<IPost>()
const postRelationshipContainer = await containerFor(server.database.client, 'PostRelationships')
const query = createQuerySpec('SELECT * FROM PostRelationships r WHERE r.partitionKey = @partitionKey AND ARRAY_CONTAINS(r.parents, @id)', {
partitionKey: post.root,
id: post.id,
})
const { resources: descendantRelationships } = await postRelationshipContainer.items.query<IPostRelationship>(query, {}).fetchAll()
const { resources: descendants } = await postContainer.items.query<IPost>({
query: 'SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@descendants, p.id)',
parameters: [{
name: '@descendants',
value: descendantRelationships.map(r => r.id),
}]
}, {}).fetchAll()
const { resources: ancestors } = await postContainer.items.query<IPost>({
query: 'SELECT * FROM Posts p WHERE ARRAY_CONTAINS(@parents, p.id)',
parameters: [{
name: '@parents',
value: post.parents,
}]
}, {}).fetchAll()
const getUserId = (post: IPost) => post.userId
const users = await getUsers(server.database.client, [
...descendants.map(getUserId),
...ancestors.map(getUserId),
getUserId(post),
])
return {
post,
descendants,
ancestors,
users,
}
})
}
const plugin: Plugin<Server, IncomingMessage, ServerResponse, PluginOptions> = async server => {
doPostRoute(server)
postsByUserRoute(server)
postRoute(server)
}

2
src/plugins/api/profile.ts

@ -55,7 +55,7 @@ function updateRoute(server: fastify.FastifyInstance<Server, IncomingMessage, Se
if (!request.user) return unauthorizedError(reply)
const container = await containerFor(server.database.client, 'Users')
const userItem = await container.item(request.user.id, request.user.id)
const userItem = container.item(request.user.id, request.user.id)
const { resource: user } = await userItem.read<IUser>()
if (!user) return serverError(reply)

1
src/types/collections.ts

@ -81,6 +81,7 @@ export interface IPost {
id: string
userId: string
root: string
parents: string[] // Post IDs
text?: string
cover?: string
attachments: IPostAttachment[]

Loading…
Cancel
Save