|
1 |
| -import { Module } from '@nestjs/common'; |
2 |
| -import Ajv from 'ajv/dist/2020'; |
| 1 | +import { Logger, Module } from '@nestjs/common'; |
| 2 | +import Ajv from 'ajv'; |
| 3 | +import Ajv2020 from 'ajv/dist/2020'; |
| 4 | +import { actionSchema, actorSchema, fnSchema, scenarioSchema, schemaSchema } from '@letsflow/core/schemas/v1.0.0'; |
| 5 | +import fs from 'fs/promises'; |
| 6 | +import { ConfigModule } from '@/common/config/config.module'; |
| 7 | +import { ConfigService } from '@/common/config/config.service'; |
| 8 | +import { yaml } from '@letsflow/core'; |
| 9 | +import { normalize, Schema } from '@letsflow/core/scenario'; |
| 10 | +import yamlOptions from '@/common/yaml-options'; |
3 | 11 |
|
4 | 12 | @Module({
|
| 13 | + imports: [ConfigModule], |
5 | 14 | providers: [
|
6 | 15 | {
|
7 | 16 | provide: Ajv,
|
8 |
| - useValue: new Ajv(), |
| 17 | + useFactory: async (config: ConfigService) => { |
| 18 | + const logger = new Logger('Ajv'); |
| 19 | + |
| 20 | + const ajv = new Ajv2020({ allErrors: true }); |
| 21 | + ajv.addKeyword('$anchor'); // Workaround for https://github.com/ajv-validator/ajv/issues/1854 |
| 22 | + ajv.addSchema([scenarioSchema, actionSchema, actorSchema, fnSchema, schemaSchema]); |
| 23 | + |
| 24 | + const schemas = await loadSchemas(config.get('schema.path'), logger); |
| 25 | + ajv.addSchema(schemas); |
| 26 | + |
| 27 | + return ajv; |
| 28 | + }, |
| 29 | + inject: [ConfigService], |
9 | 30 | },
|
10 | 31 | ],
|
11 | 32 | exports: [Ajv],
|
12 | 33 | })
|
13 | 34 | export class AjvModule {}
|
| 35 | + |
| 36 | +async function loadSchemas(dir: string, logger: Logger): Promise<Schema[]> { |
| 37 | + const files = await fs.readdir(dir); |
| 38 | + const schemaContent = await Promise.all( |
| 39 | + files |
| 40 | + .filter((file) => file.endsWith('.json') || file.endsWith('.yaml') || file.endsWith('.yml')) |
| 41 | + .map((file) => fs.readFile(`${dir}/${file}`, 'utf8').then((content) => [file, content] as const)), |
| 42 | + ); |
| 43 | + |
| 44 | + return schemaContent |
| 45 | + .map(([file, content]): [string, Schema | undefined] => { |
| 46 | + try { |
| 47 | + const schema = file.endsWith('.json') ? JSON.parse(content) : yaml.parse(content, yamlOptions); |
| 48 | + return [file, schema]; |
| 49 | + } catch (e) { |
| 50 | + logger.warn(`Failed to parse schema ${file}`); |
| 51 | + return [file, undefined]; |
| 52 | + } |
| 53 | + }) |
| 54 | + .filter(([file, schema]) => { |
| 55 | + if (typeof schema === 'undefined') { |
| 56 | + return false; |
| 57 | + } |
| 58 | + |
| 59 | + if (!schema || !schema.$id) { |
| 60 | + logger.warn(`Skipping schema '${file}': Missing '$id' property`); |
| 61 | + return false; |
| 62 | + } |
| 63 | + |
| 64 | + if (schema.$schema && typeof schema.$schema !== 'string') { |
| 65 | + logger.warn(`Skipping schema '${file}': '$schema' must be a string`); |
| 66 | + return false; |
| 67 | + } |
| 68 | + |
| 69 | + if (schema.$schema && schema.$schema !== schemaSchema.id && schema.$schema !== schemaSchema.$schema) { |
| 70 | + logger.warn(`Skipping schema '${file}': Unsupported schema ${schema.$schema}`); |
| 71 | + return false; |
| 72 | + } |
| 73 | + |
| 74 | + logger.debug(`Loaded schema ${schema.$id}`); |
| 75 | + return true; |
| 76 | + }) |
| 77 | + .map(([, schema]) => normalize(schema, { $schema: schemaSchema.$id })); |
| 78 | +} |
0 commit comments