Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions typescript-api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions typescript-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"@feathersjs/schema": "^5.0.0-pre.27",
"@feathersjs/socketio": "^5.0.0-pre.27",
"@feathersjs/transport-commons": "^5.0.0-pre.27",
"@sinclair/typebox": "^0.24.42",
"knex": "^2.2.0",
"koa-static": "^5.0.0",
"sqlite3": "^5.0.9",
Expand Down
32 changes: 32 additions & 0 deletions typescript-api/src/@authentication/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Type, Nullable } from "../@schema"

export const authenticationSettingsSchema = Type.Object({
secret: Type.String({ description: 'The JWT signing secret' }),
entity: Nullable(Type.String(), { description: 'The name of the authentication entity (e.g. user)' }),
entityId: Type.String({ description: 'The name of the authentication entity id property' }),
service: Type.String({ description: 'The path of the entity service' }),
authStrategies: Type.Array(Type.String(), { description: 'A list of authentication strategy names that are allowed to create JWT access tokens' }),
parseStrategies: Type.Array(Type.String(), { description: 'A list of authentication strategy names that should parse HTTP headers for authentication information (defaults to `authStrategies`)' }),
jwtOptions: Type.Object({}),
jwt: Type.Object({
header: Type.String({ default: 'Authorization', description: 'The HTTP header containing the JWT' }),
schemes: Type.String({ description: 'An array of schemes to support' }),
}),
local: Type.Object({
usernameField: Type.String({ description: 'Name of the username field (e.g. `email`)' }),
passwordField: Type.String({ description: 'Name of the password field (e.g. `password`)' }),
hashSize: Type.Optional(Type.Number({ description: 'The BCrypt salt length' })),
errorMessage: Type.Optional(Type.String({ description: 'The error message to return on errors' })),
entityUsernameField: Type.Optional(Type.String({ description: 'Name of the username field on the entity if authentication request data and entity field names are different' })),
entityPasswordField: Type.Optional(Type.String({ description: 'Name of the password field on the entity if authentication request data and entity field names are different' })),
}),
oauth: Type.Object({
redirect: Type.Optional(Type.String()),
origins: Type.Optional(Type.Array(Type.String())),
defaults: Type.Optional(Type.Object({
key: Type.Optional(Type.String()),
secret: Type.Optional(Type.String()),
})),
})
})

1 change: 1 addition & 0 deletions typescript-api/src/@schema/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './resolve'
199 changes: 199 additions & 0 deletions typescript-api/src/@schema/hooks/resolve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { HookContext, NextFunction } from '@feathersjs/feathers'
import { compose } from '@feathersjs/hooks'
import { Static, TObject } from '@sinclair/typebox'
import { Resolver, ResolverStatus } from '../resolver'

const getContext = <H extends HookContext>(context: H) => {
return Object.freeze({
...context,
params: Object.freeze({
...context.params,
query: Object.freeze({})
})
})
}

const getData = <H extends HookContext>(context: H) => {
const isPaginated = context.method === 'find' && context.result.data
const data = isPaginated ? context.result.data : context.result

return { isPaginated, data }
}

const runResolvers = async <
S extends TObject,
H extends HookContext,
T = Static<S>
>(
resolvers: Resolver<S, H>[],
data: any,
ctx: H,
status?: Partial<ResolverStatus<S, H>>
) => {
let current: any = data

for (const resolver of resolvers) {
if (resolver && typeof resolver.resolve === 'function') {
current = await resolver.resolve(current, ctx, status)
}
}

return current as T
}

export type ResolverSetting<H extends HookContext> = Resolver<any, H> | Resolver<any, H>[]

export type DataResolvers<H extends HookContext> = {
create: Resolver<any, H>
patch: Resolver<any, H>
update: Resolver<any, H>
}

export type ResolveAllSettings<H extends HookContext> = {
data?: DataResolvers<H>
query?: Resolver<any, H>
result?: Resolver<any, H>
dispatch?: Resolver<any, H>
}

export const DISPATCH = Symbol('@feathersjs/schema/dispatch')

export const getDispatch = (value: any) =>
typeof value === 'object' && value !== null && value[DISPATCH] !== undefined ? value[DISPATCH] : value

export const resolveQuery =
<S extends TObject, H extends HookContext>(...resolvers: Resolver<S, H>[]) =>
async (context: H, next?: NextFunction) => {
const ctx = getContext(context)
const data = context?.params?.query || {}
const query = await runResolvers(resolvers, data, ctx)

context.params = {
...context.params,
query
}

if (typeof next === 'function') {
return next()
}
}

export const resolveData =
<H extends HookContext>(settings: DataResolvers<H> | Resolver<any, H>) =>
async (context: H, next?: NextFunction) => {
if (context.method === 'create' || context.method === 'patch' || context.method === 'update') {
const resolvers = settings instanceof Resolver ? [settings] : [settings[context.method]]
const ctx = getContext(context)
const data = context.data

const status = {
originalContext: context
}

if (Array.isArray(data)) {
context.data = await Promise.all(data.map((current) => runResolvers(resolvers, current, ctx, status)))
} else {
context.data = await runResolvers(resolvers, data, ctx, status)
}
}

if (typeof next === 'function') {
return next()
}
}

export const resolveResult =
<S extends TObject, H extends HookContext>(...resolvers: Resolver<S, H>[]) =>
async (context: H, next?: NextFunction) => {
if (typeof next === 'function') {
const { $resolve: properties, ...query } = context.params?.query || {}
const resolve = {
originalContext: context,
...context.params.resolve,
properties
}

context.params = {
...context.params,
resolve,
query
}

await next()
}

const ctx = getContext(context)
const status = context.params.resolve
const { isPaginated, data } = getData(context)

const result = Array.isArray(data)
? await Promise.all(data.map(async (current) => runResolvers(resolvers, current, ctx, status)))
: await runResolvers(resolvers, data, ctx, status)

if (isPaginated) {
context.result.data = result
} else {
context.result = result
}
}

export const resolveDispatch = <
S extends TObject,
H extends HookContext,
T = Static<S>
>(...resolvers: Resolver<S, H, T>[]) =>
async (context: H, next?: NextFunction) => {
if (typeof next === 'function') {
await next()
}

const ctx = getContext(context)
const status = context.params.resolve
const { isPaginated, data } = getData(context)
const resolveAndGetDispatch = async (current: any) => {
const resolved: any = await runResolvers(resolvers, current, ctx, status)

return Object.keys(resolved).reduce((res, key) => {
res[key] = getDispatch(resolved[key])

return res
}, {} as any)
}

const result = await (Array.isArray(data)
? Promise.all(data.map(resolveAndGetDispatch))
: resolveAndGetDispatch(data))
const dispatch = isPaginated
? {
...context.result,
data: result
}
: result

context.dispatch = dispatch
Object.defineProperty(context.result, DISPATCH, {
value: dispatch,
enumerable: false,
configurable: false
})
}

export const resolveAll = <H extends HookContext>(map: ResolveAllSettings<H>) => {
const middleware = []

middleware.push(resolveDispatch(map.dispatch))

if (map.result) {
middleware.push(resolveResult(map.result))
}

if (map.query) {
middleware.push(resolveQuery(map.query))
}

if (map.data) {
middleware.push(resolveData(map.data))
}

return compose(middleware)
}
25 changes: 25 additions & 0 deletions typescript-api/src/@schema/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SchemaOptions, Static, TSchema, Type } from '@sinclair/typebox'
import { ResolverStatus } from './resolver'

export * from './schema'
export * from './resolver'
export * from './hooks'
export * from './query'

export type Infer<S, P extends unknown[] = []> =
S extends { _type: any }
? S['_type']
: S extends TSchema
? Static<S, P>
: never;

export const Nullable = <T extends TSchema>(type: T, options?: SchemaOptions) => Type.Union([type, Type.Null()], options)

export { Type } from '@sinclair/typebox'

declare module '@feathersjs/feathers/lib/declarations' {
interface Params {
resolve?: ResolverStatus<any, HookContext>
}
}

68 changes: 68 additions & 0 deletions typescript-api/src/@schema/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { _ } from '@feathersjs/commons'
import { Type, TObject, TSchema, TIntersect } from '@sinclair/typebox'

const ArrayOfKeys = <T extends TObject>(type: T) => {
const keys = Object.keys(type.properties);
return Type.Unsafe<(keyof T['properties'])[]>({ type: 'array', items: { type: 'string', enum: keys } })
}

export const queryProperty = <T extends TSchema>(def: T) => {
return Type.Union([
def,
Type.Object({
$gt: def,
$gte: def,
$lt: def,
$lte: def,
$ne: def,
$in: Type.Array(def),
$nin: Type.Array(def)
})
], { additionalProperties: false })
}

type QueryProperty<T extends TSchema> = ReturnType<typeof queryProperty<T>>

export const queryProperties = <T extends TObject>(type: T) => {
const properties = Object.keys(type.properties).reduce((res, key) => {
const result = res as any

result[key] = queryProperty(type[key])

return result
}, {} as { [K in keyof T['properties']]: QueryProperty<T['properties'][K]> })

const result = {
type: 'object',
additionalProperties: false,
properties
} as TObject<typeof properties>

return result;
}

export function SortKeys<T extends TObject>(schema: T) {
const keys = Object.keys(schema.properties);

const result = keys.reduce((res, key) => {
const result = res as any

result[key] = Type.Unsafe<1 | -1>({ type: 'number', enum: [1, -1] })

return result
}, {} as { [K in keyof T['properties']]: { readonly type: 'number'; readonly enum: [1, -1] } })

return Type.Unsafe<{ [K in keyof T['properties']]?: 1 | -1 }>(result)
}

export const querySyntax = <T extends TObject | TIntersect>(type: T) => {
return Type.Intersect([
Type.Object({
$limit: Type.Optional(Type.Number({ minimum: 0 })),
$skip: Type.Optional(Type.Number({ minimum: 0 })),
$sort: SortKeys(type),
$select: ArrayOfKeys(type)
}, { additionalProperties: false }),
queryProperties(type)
])
}
Loading