Skip to content
Merged
Prev Previous commit
Next Next commit
Add Typebox integration
  • Loading branch information
daffl committed Oct 4, 2022
commit 8903214a38e3b37b2be22a15fedf22c208b37c83
11 changes: 11 additions & 0 deletions package-lock.json

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

31 changes: 23 additions & 8 deletions packages/cli/src/app/templates/schemas.tpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,31 @@ import { generator, toFile } from '@feathershq/pinion'
import { renderSource } from '../../commons'
import { AppGeneratorContext } from '../index'

const validatorTemplate = /* ts */ `import { Ajv } from '@feathersjs/schema'
const validatorTemplate = /* ts */ `import { Ajv, addFormats } from '@feathersjs/schema'
import type { FormatsPluginOptions } from '@feathersjs/schema'

export const dataValidator = new Ajv({
addUsedSchema: false
})
const formats: FormatsPluginOptions = [
'date-time',
'time',
'date',
'email',
'hostname',
'ipv4',
'ipv6',
'uri',
'uri-reference',
'uuid',
'uri-template',
'json-pointer',
'relative-json-pointer',
'regex'
]

export const queryValidator = new Ajv({
coerceTypes: true,
addUsedSchema: false
})
export const dataValidator = addFormats(new Ajv({}), formats)

export const queryValidator = addFormats(new Ajv({
coerceTypes: true
}), formats)

`
const configurationTemplate =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { generator, toFile } from '@feathershq/pinion'
import { generator, toFile, when } from '@feathershq/pinion'
import { renderSource } from '../../commons'
import { AuthenticationGeneratorContext } from '../index'

Expand Down Expand Up @@ -100,14 +100,17 @@ export const ${camelName}QueryResolver = resolve<${upperName}Query, HookContext>

export const generate = (ctx: AuthenticationGeneratorContext) =>
generator(ctx).then(
renderSource(
template,
toFile(({ lib, folder, fileName }: AuthenticationGeneratorContext) => [
lib,
'services',
...folder,
`${fileName}.schema`
]),
{ force: true }
when<AuthenticationGeneratorContext>(
({ schema }) => schema === 'json',
renderSource(
template,
toFile(({ lib, folder, fileName }: AuthenticationGeneratorContext) => [
lib,
'services',
...folder,
`${fileName}.schema`
]),
{ force: true }
)
)
)
101 changes: 101 additions & 0 deletions packages/cli/src/authentication/templates/schema.typebox.tpl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { generator, toFile, when } from '@feathershq/pinion'
import { renderSource } from '../../commons'
import { AuthenticationGeneratorContext } from '../index'

export const template = ({
camelName,
upperName,
authStrategies,
type,
relative
}: AuthenticationGeneratorContext) => /* ts */ `import { Type, typebox, jsonSchema, resolve } from '@feathersjs/schema'
import type { Static } from '@feathersjs/schema'
${authStrategies.includes('local') ? `import { passwordHash } from '@feathersjs/authentication-local'` : ''}

import type { HookContext } from '${relative}/declarations'
import { dataValidator, queryValidator } from '${relative}/schemas/validators'

// Schema for the basic data model (e.g. creating new entries)
export const ${camelName}DataSchema = Type.Object({
${authStrategies
.map((name) =>
name === 'local'
? ` email: Type.String(),
password: Type.String()`
: ` ${name}Id: Type.Optional(Type.String())`
)
.join(',\n')}
}, { $id: '${upperName}Data', additionalProperties: false })

export type ${upperName}Data = Static<typeof ${camelName}DataSchema>

export const ${camelName}DataValidator = jsonSchema.getDataValidator(${camelName}DataSchema, dataValidator)

export const ${camelName}DataResolver = resolve<${upperName}Data, HookContext>({
properties: {
${authStrategies.includes('local') ? `password: passwordHash({ strategy: 'local' })` : ''}
}
})

// Schema for the data that is being returned
export const ${camelName}Schema = Type.Intersect([
${camelName}DataSchema,
Type.Object({
${type === 'mongodb' ? '_id: Type.String()' : 'id: Type.Number()'}
})
], { $id: '${upperName}' })

export type ${upperName} = Static<typeof ${camelName}Schema>

export const ${camelName}Resolver = resolve<${upperName}, HookContext>({
properties: {}
})

export const ${camelName}ExternalResolver = resolve<${upperName}, HookContext>({
properties: {
// The password should never be visible externally
password: async () => undefined
}
})

// Schema for allowed query properties
export const ${camelName}QuerySchema = Type.Intersect([
typebox.querySyntax(${camelName}Schema),
// Add additional query properties here
Type.Object({})
])

export type ${upperName}Query = Static<typeof ${camelName}QuerySchema>

export const ${camelName}QueryValidator = jsonSchema.getValidator(${camelName}QuerySchema, queryValidator)

export const ${camelName}QueryResolver = resolve<${upperName}Query, HookContext>({
properties: {
// If there is a user (e.g. with authentication), they are only allowed to see their own data
${type === 'mongodb' ? '_id' : 'id'}: async (value, user, context) => {
if (context.params.user) {
return context.params.user.${type === 'mongodb' ? '_id' : 'id'}
}

return value
}
}
})
`

export const generate = (ctx: AuthenticationGeneratorContext) =>
generator(ctx).then(
when<AuthenticationGeneratorContext>(
({ schema }) => schema === 'typebox',
renderSource(
template,
toFile(({ lib, folder, fileName }: AuthenticationGeneratorContext) => [
lib,
'services',
...folder,
`${fileName}.schema`
]),
{ force: true }
)
)
)
14 changes: 8 additions & 6 deletions packages/cli/src/service/service.tpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,12 @@ export const serviceImportTemplate = ({
schema
}: ServiceGeneratorContext) => `
${authentication || isEntityService ? `import { authenticate } from '@feathersjs/authentication'` : ''}
${schema ? `import { hooks as schemaHooks } from '@feathersjs/schema'` : ''}

import type { Application } from '${relative}/declarations'

${
schema
? `import {
? `
import { hooks as schemaHooks } from '@feathersjs/schema'

import {
${camelName}DataValidator,
${camelName}QueryValidator,
${camelName}Resolver,
Expand All @@ -60,7 +59,10 @@ export type ${upperName} = any
export type ${upperName}Data = any
export type ${upperName}Query = any
`
}`
}

import type { Application } from '${relative}/declarations'
`

export const serviceRegistrationTemplate = ({
camelName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const ${camelName}QueryResolver = resolve<${upperName}Query, HookContext>
export const generate = (ctx: ServiceGeneratorContext) =>
generator(ctx).then(
when<ServiceGeneratorContext>(
({ schema }) => schema !== false,
({ schema }) => schema === 'json',
renderSource(
template,
toFile(({ lib, folder, fileName }: ServiceGeneratorContext) => [
Expand Down
76 changes: 76 additions & 0 deletions packages/cli/src/service/templates/schema.typebox.tpl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { generator, toFile, when } from '@feathershq/pinion'
import { renderSource } from '../../commons'
import { ServiceGeneratorContext } from '../index'

const template = ({
camelName,
upperName,
relative,
type
}: ServiceGeneratorContext) => /* ts */ `import { Type, jsonSchema, typebox, resolve } from '@feathersjs/schema'
import type { Static } from '@feathersjs/schema'

import type { HookContext } from '${relative}/declarations'
import { dataValidator, queryValidator } from '${relative}/schemas/validators'

// Schema for the basic data model (e.g. creating new entries)
export const ${camelName}DataSchema = Type.Object({
text: Type.String()
}, { $id: '${upperName}Data', additionalProperties: false })

export type ${upperName}Data = Static<typeof ${camelName}DataSchema>

export const ${camelName}DataValidator = jsonSchema.getDataValidator(${camelName}DataSchema, dataValidator)

export const ${camelName}DataResolver = resolve<${upperName}Data, HookContext>({
properties: {}
})

export const ${camelName}Schema = Type.Intersect([
${camelName}DataSchema,
Type.Object({
${type === 'mongodb' ? '_id: Type.String()' : 'id: Type.Number()'}
})
], { $id: '${upperName}', additionalProperties: false })

export type ${upperName} = Static<typeof ${camelName}Schema>

export const ${camelName}Resolver = resolve<${upperName}, HookContext>({
properties: {}
})

export const ${camelName}ExternalResolver = resolve<${upperName}, HookContext>({
properties: {}
})

// Schema for allowed query properties
export const ${camelName}QuerySchema = Type.Intersect([
typebox.querySyntax(${camelName}Schema),
// Add additional query properties here
Type.Object({})
])

export type ${upperName}Query = Static<typeof ${camelName}QuerySchema>

export const ${camelName}QueryValidator = jsonSchema.getValidator(${camelName}QuerySchema, queryValidator)

export const ${camelName}QueryResolver = resolve<${upperName}Query, HookContext>({
properties: {}
})
`

export const generate = (ctx: ServiceGeneratorContext) =>
generator(ctx).then(
when<ServiceGeneratorContext>(
({ schema }) => schema === 'typebox',
renderSource(
template,
toFile(({ lib, folder, fileName }: ServiceGeneratorContext) => [
lib,
'services',
...folder,
`${fileName}.schema`
])
)
)
)
2 changes: 2 additions & 0 deletions packages/schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@
"@feathersjs/errors": "^5.0.0-pre.29",
"@feathersjs/feathers": "^5.0.0-pre.29",
"@feathersjs/hooks": "^0.7.5",
"@sinclair/typebox": "^0.24.44",
"@types/json-schema": "^7.0.11",
"ajv": "^8.11.0",
"ajv-formats": "^2.1.1",
"json-schema": "^0.4.0",
"json-schema-to-ts": "^2.5.5"
},
Expand Down
6 changes: 6 additions & 0 deletions packages/schema/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import addFormats, { FormatName, FormatOptions, FormatsPluginOptions } from 'ajv-formats'
import { ResolverStatus } from './resolver'

export type { FromSchema } from 'json-schema-to-ts'
export { addFormats, FormatName, FormatOptions, FormatsPluginOptions }

export * from '@sinclair/typebox'

export * from './schema'
export * from './resolver'
export * from './hooks'
export * from './json-schema'

export * as hooks from './hooks'
export * as jsonSchema from './json-schema'
export * as typebox from './typebox'

export type Infer<S extends { _type: any }> = S['_type']

Expand Down
Loading