WAL journal mode + OpenAPI 3.1

- InitSchema migration now issues PRAGMA journal_mode = WAL after
  PRAGMA foreign_keys = ON so fresh DBs are created in WAL mode.
- DatabaseInitService.init() also calls ensureWalMode() after
  dataSource.initialize() so warm DBs (where migrations are skipped)
  have WAL applied and persisted on every startup. Logs whether it
  was already set or just enabled.
- main.ts now serves an OpenAPI 3.1 document at /api/docs and
  /api/docs-json. A new toOpenApi31() utility converts the
  NestJS-generated 3.0 document by setting openapi=3.1.0, normalising
  the required info fields, and rewriting every legacy
  'nullable: true' marker into a JSON Schema 2020-12 type union
  ('type: [<orig>, "null"]'). Recursion handles components, paths,
  parameters, request bodies, response bodies, properties, items,
  oneOf/anyOf/allOf, and parameter/request schemas.
- 7 new unit tests in tests/backend/openapi31.spec.ts lock the
  conversion behaviour; total suite: 83 tests across 18 specs.
This commit is contained in:
OpenVelo Agent
2026-07-21 14:13:19 +00:00
parent ac6c834525
commit de27a8bb24
6 changed files with 243 additions and 153 deletions
+97
View File
@@ -0,0 +1,97 @@
import { toOpenApi31 } from '../../backend/src/common/utils/openapi31';
describe('toOpenApi31', () => {
it('sets openapi to 3.1.0', () => {
const doc: any = toOpenApi31({ openapi: '3.0.0', info: { title: 'X', version: '1' }, paths: {} });
expect(doc.openapi).toBe('3.1.0');
});
it('preserves info title and version', () => {
const doc: any = toOpenApi31({ openapi: '3.0.0', info: { title: 'HIPCTF', version: '1.0.0' }, paths: {} });
expect(doc.info.title).toBe('HIPCTF');
expect(doc.info.version).toBe('1.0.0');
});
it('rewrites nullable=true to type union with null', () => {
const doc: any = toOpenApi31({
openapi: '3.0.0',
info: { title: 't', version: 'v' },
paths: {},
components: {
schemas: {
Foo: { type: 'object', properties: { name: { type: 'string', nullable: true } } },
},
},
});
expect(doc.components.schemas.Foo.properties.name).toEqual({ type: ['string', 'null'] });
expect(doc.components.schemas.Foo.properties.name.nullable).toBeUndefined();
});
it('handles nullable without an explicit type', () => {
const doc: any = toOpenApi31({
openapi: '3.0.0',
info: { title: 't', version: 'v' },
paths: {},
components: { schemas: { Bar: { nullable: true } } },
});
expect(doc.components.schemas.Bar).toEqual({ type: ['null'] });
});
it('preserves $ref nodes', () => {
const doc: any = toOpenApi31({
openapi: '3.0.0',
info: { title: 't', version: 'v' },
paths: {},
components: {
schemas: {
Foo: { $ref: '#/components/schemas/Bar' },
},
},
});
expect(doc.components.schemas.Foo).toEqual({ $ref: '#/components/schemas/Bar' });
});
it('recurses into array items, oneOf, anyOf, allOf', () => {
const doc: any = toOpenApi31({
openapi: '3.0.0',
info: { title: 't', version: 'v' },
paths: {},
components: {
schemas: {
Foo: {
type: 'object',
properties: {
arr: { type: 'array', items: { type: 'string', nullable: true } },
union: { oneOf: [{ type: 'string', nullable: true }] },
},
},
},
},
});
expect(doc.components.schemas.Foo.properties.arr.items).toEqual({ type: ['string', 'null'] });
expect(doc.components.schemas.Foo.properties.union.oneOf[0]).toEqual({ type: ['string', 'null'] });
});
it('rewrites nullable in path parameters and request bodies', () => {
const doc: any = toOpenApi31({
openapi: '3.0.0',
info: { title: 't', version: 'v' },
paths: {
'/x': {
get: {
parameters: [{ name: 'q', in: 'query', schema: { type: 'string', nullable: true } }],
requestBody: {
content: {
'application/json': { schema: { type: 'object', properties: { p: { type: 'integer', nullable: true } } } },
},
},
},
},
},
});
const params = doc.paths['/x'].get.parameters;
expect(params[0].schema).toEqual({ type: ['string', 'null'] });
const bodySchema = doc.paths['/x'].get.requestBody.content['application/json'].schema;
expect(bodySchema.properties.p).toEqual({ type: ['integer', 'null'] });
});
});