vouchington-tooling 0.0.10 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +2 -0
- package/dist/openapi-document/build-openapi-document.d.mts +11 -0
- package/dist/openapi-document/build-openapi-document.mjs +122 -0
- package/dist/openapi-document/build-openapi-query.d.mts +3 -0
- package/dist/openapi-document/build-openapi-query.mjs +53 -0
- package/dist/openapi-document/build-openapi-request.d.mts +7 -0
- package/dist/openapi-document/build-openapi-request.mjs +29 -0
- package/dist/openapi-document/build-openapi-response.d.mts +9 -0
- package/dist/openapi-document/build-openapi-response.mjs +111 -0
- package/dist/openapi-document/component-registry.d.mts +9 -0
- package/dist/openapi-document/component-registry.mjs +49 -0
- package/dist/openapi-document/contract-schema-canonical.d.mts +4 -0
- package/dist/openapi-document/contract-schema-canonical.mjs +23 -0
- package/dist/openapi-document/contract-schema-intersection-merge.d.mts +13 -0
- package/dist/openapi-document/contract-schema-intersection-merge.mjs +96 -0
- package/dist/openapi-document/contract-schema-to-openapi.d.mts +7 -0
- package/dist/openapi-document/contract-schema-to-openapi.mjs +71 -0
- package/dist/openapi-document/contract-schema-types.d.mts +43 -0
- package/dist/openapi-document/contract-schema-types.mjs +1 -0
- package/dist/openapi-document/index.d.mts +19 -0
- package/dist/openapi-document/index.mjs +12 -0
- package/dist/openapi-document/openapi-error-response.d.mts +2 -0
- package/dist/openapi-document/openapi-error-response.mjs +13 -0
- package/dist/openapi-document/openapi-route-helpers.d.mts +9 -0
- package/dist/openapi-document/openapi-route-helpers.mjs +61 -0
- package/dist/openapi-document/openapi-types.d.mts +80 -0
- package/dist/openapi-document/openapi-types.mjs +1 -0
- package/dist/openapi-document/operation-types.d.mts +35 -0
- package/dist/openapi-document/operation-types.mjs +11 -0
- package/dist/openapi-document/query-types.d.mts +35 -0
- package/dist/openapi-document/query-types.mjs +1 -0
- package/dist/openapi-document/schema-node-builders.d.mts +3 -0
- package/dist/openapi-document/schema-node-builders.mjs +6 -0
- package/dist/openapi-document/write-openapi.d.mts +9 -0
- package/dist/openapi-document/write-openapi.mjs +19 -0
- package/dist/pg-schema-snapshot/build-snapshot.d.mts +9 -0
- package/dist/pg-schema-snapshot/build-snapshot.mjs +61 -0
- package/dist/pg-schema-snapshot/catalog-objects.d.mts +34 -0
- package/dist/pg-schema-snapshot/catalog-objects.mjs +64 -0
- package/dist/pg-schema-snapshot/catalog-queries.d.mts +20 -0
- package/dist/pg-schema-snapshot/catalog-queries.mjs +31 -0
- package/dist/pg-schema-snapshot/catalog-shared.d.mts +3 -0
- package/dist/pg-schema-snapshot/catalog-shared.mjs +19 -0
- package/dist/pg-schema-snapshot/catalog-table-constraints.d.mts +14 -0
- package/dist/pg-schema-snapshot/catalog-table-constraints.mjs +54 -0
- package/dist/pg-schema-snapshot/catalog-table-indexes.d.mts +30 -0
- package/dist/pg-schema-snapshot/catalog-table-indexes.mjs +81 -0
- package/dist/pg-schema-snapshot/catalog-tables.d.mts +22 -0
- package/dist/pg-schema-snapshot/catalog-tables.mjs +48 -0
- package/dist/pg-schema-snapshot/file-safety.d.mts +4 -0
- package/dist/pg-schema-snapshot/file-safety.mjs +71 -0
- package/dist/pg-schema-snapshot/generate.d.mts +18 -0
- package/dist/pg-schema-snapshot/generate.mjs +95 -0
- package/dist/pg-schema-snapshot/index-rename-detect.d.mts +40 -0
- package/dist/pg-schema-snapshot/index-rename-detect.mjs +71 -0
- package/dist/pg-schema-snapshot/index.d.mts +10 -0
- package/dist/pg-schema-snapshot/index.mjs +6 -0
- package/dist/pg-schema-snapshot/markdown-files.d.mts +1 -0
- package/dist/pg-schema-snapshot/markdown-files.mjs +26 -0
- package/dist/pg-schema-snapshot/render-markdown.d.mts +8 -0
- package/dist/pg-schema-snapshot/render-markdown.mjs +113 -0
- package/dist/pg-schema-snapshot/render-tables.d.mts +2 -0
- package/dist/pg-schema-snapshot/render-tables.mjs +55 -0
- package/dist/pg-schema-snapshot/snapshot-build-utils.d.mts +2 -0
- package/dist/pg-schema-snapshot/snapshot-build-utils.mjs +15 -0
- package/dist/pg-schema-snapshot/snapshot.test-helpers.d.mts +11 -0
- package/dist/pg-schema-snapshot/snapshot.test-helpers.mjs +101 -0
- package/dist/pg-schema-snapshot/table-snapshot-columns.d.mts +3 -0
- package/dist/pg-schema-snapshot/table-snapshot-columns.mjs +33 -0
- package/dist/pg-schema-snapshot/table-snapshot-constraints.d.mts +7 -0
- package/dist/pg-schema-snapshot/table-snapshot-constraints.mjs +55 -0
- package/dist/pg-schema-snapshot/table-snapshot.d.mts +10 -0
- package/dist/pg-schema-snapshot/table-snapshot.mjs +99 -0
- package/dist/pg-schema-snapshot/types.d.mts +103 -0
- package/dist/pg-schema-snapshot/types.mjs +1 -0
- package/package.json +11 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { intersectionToOpenApi } from './contract-schema-intersection-merge.mjs';
|
|
2
|
+
export function nodeToOpenApi(node, ctx) {
|
|
3
|
+
switch (node.type) {
|
|
4
|
+
case 'unknown':
|
|
5
|
+
return {};
|
|
6
|
+
case 'null':
|
|
7
|
+
return { type: 'null' };
|
|
8
|
+
case 'boolean':
|
|
9
|
+
case 'number':
|
|
10
|
+
case 'string':
|
|
11
|
+
return { type: node.type };
|
|
12
|
+
case 'literal':
|
|
13
|
+
return { const: node.value };
|
|
14
|
+
case 'array':
|
|
15
|
+
return { type: 'array', items: nodeToOpenApi(node.items, ctx) };
|
|
16
|
+
case 'tuple':
|
|
17
|
+
return tupleToOpenApi(node, ctx);
|
|
18
|
+
case 'object':
|
|
19
|
+
return objectToOpenApi(node, ctx);
|
|
20
|
+
case 'union':
|
|
21
|
+
return unionToOpenApi(node, ctx);
|
|
22
|
+
case 'intersection':
|
|
23
|
+
return intersectionToOpenApi(node, ctx, nodeToOpenApi);
|
|
24
|
+
case 'ref':
|
|
25
|
+
return { $ref: `#/components/schemas/${ctx.refName(node.name)}` };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function tupleToOpenApi(node, ctx) {
|
|
29
|
+
const prefixItems = node.items.map((item) => nodeToOpenApi(item, ctx));
|
|
30
|
+
const minItems = node.items.length - node.optionalItems;
|
|
31
|
+
if (node.rest) {
|
|
32
|
+
return { type: 'array', prefixItems, minItems, items: nodeToOpenApi(node.rest, ctx) };
|
|
33
|
+
}
|
|
34
|
+
return { type: 'array', prefixItems, minItems, items: false, maxItems: node.items.length };
|
|
35
|
+
}
|
|
36
|
+
function objectToOpenApi(node, ctx) {
|
|
37
|
+
const keys = Object.keys(node.properties).toSorted();
|
|
38
|
+
const properties = {};
|
|
39
|
+
const required = [];
|
|
40
|
+
for (const key of keys) {
|
|
41
|
+
const property = node.properties[key];
|
|
42
|
+
properties[key] = nodeToOpenApi(property.schema, ctx);
|
|
43
|
+
if (property.required)
|
|
44
|
+
required.push(key);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties,
|
|
49
|
+
...(required.length > 0 ? { required } : {}),
|
|
50
|
+
additionalProperties: node.additionalProperties === false ? false : nodeToOpenApi(node.additionalProperties, ctx),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const LITERAL_TYPES = { boolean: 'boolean', number: 'number', string: 'string' };
|
|
54
|
+
function unionToOpenApi(node, ctx) {
|
|
55
|
+
return (literalUnionEnum(node) ?? { anyOf: node.variants.map((variant) => nodeToOpenApi(variant, ctx)) });
|
|
56
|
+
}
|
|
57
|
+
function literalUnionEnum(node) {
|
|
58
|
+
if (node.variants.length === 0)
|
|
59
|
+
return undefined;
|
|
60
|
+
if (!node.variants.every((variant) => variant.type === 'literal'))
|
|
61
|
+
return undefined;
|
|
62
|
+
const literals = node.variants;
|
|
63
|
+
const type = LITERAL_TYPES[typeof literals[0].value];
|
|
64
|
+
const sameType = literals.every((literal) => LITERAL_TYPES[typeof literal.value] === type);
|
|
65
|
+
if (!sameType)
|
|
66
|
+
return undefined;
|
|
67
|
+
const values = [...new Set(literals.map((literal) => literal.value))].toSorted((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
68
|
+
if (type === 'boolean' && values.length === 2)
|
|
69
|
+
return { type: 'boolean' };
|
|
70
|
+
return { type, enum: values };
|
|
71
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type ContractSchema = {
|
|
2
|
+
root: ContractSchemaNode;
|
|
3
|
+
definitions: Record<string, ContractSchemaNode>;
|
|
4
|
+
};
|
|
5
|
+
export type ContractSchemaNode = {
|
|
6
|
+
type: 'unknown';
|
|
7
|
+
} | {
|
|
8
|
+
type: 'null';
|
|
9
|
+
} | {
|
|
10
|
+
type: 'boolean';
|
|
11
|
+
} | {
|
|
12
|
+
type: 'number';
|
|
13
|
+
} | {
|
|
14
|
+
type: 'string';
|
|
15
|
+
} | {
|
|
16
|
+
type: 'literal';
|
|
17
|
+
value: boolean | number | string;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'array';
|
|
20
|
+
items: ContractSchemaNode;
|
|
21
|
+
} | {
|
|
22
|
+
type: 'tuple';
|
|
23
|
+
items: ContractSchemaNode[];
|
|
24
|
+
optionalItems: number;
|
|
25
|
+
rest?: ContractSchemaNode;
|
|
26
|
+
} | {
|
|
27
|
+
type: 'object';
|
|
28
|
+
properties: Record<string, ContractSchemaProperty>;
|
|
29
|
+
additionalProperties: false | ContractSchemaNode;
|
|
30
|
+
} | {
|
|
31
|
+
type: 'union';
|
|
32
|
+
variants: ContractSchemaNode[];
|
|
33
|
+
} | {
|
|
34
|
+
type: 'intersection';
|
|
35
|
+
variants: ContractSchemaNode[];
|
|
36
|
+
} | {
|
|
37
|
+
type: 'ref';
|
|
38
|
+
name: string;
|
|
39
|
+
};
|
|
40
|
+
export type ContractSchemaProperty = {
|
|
41
|
+
required: boolean;
|
|
42
|
+
schema: ContractSchemaNode;
|
|
43
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { buildOpenApiDocument } from './build-openapi-document.mts';
|
|
2
|
+
export type { BuildOpenApiDocumentInput } from './build-openapi-document.mts';
|
|
3
|
+
export { queryParameters } from './build-openapi-query.mts';
|
|
4
|
+
export { buildOperationRequestBody } from './build-openapi-request.mts';
|
|
5
|
+
export { buildOperationResponse } from './build-openapi-response.mts';
|
|
6
|
+
export { createComponentRegistry, sanitizeComponentName } from './component-registry.mts';
|
|
7
|
+
export type { ComponentRegistry } from './component-registry.mts';
|
|
8
|
+
export { canonicalContractSchema, hashContractSchema } from './contract-schema-canonical.mts';
|
|
9
|
+
export { nodeToOpenApi } from './contract-schema-to-openapi.mts';
|
|
10
|
+
export type { OpenApiConverterContext } from './contract-schema-to-openapi.mts';
|
|
11
|
+
export type { ContractSchema, ContractSchemaNode, ContractSchemaProperty, } from './contract-schema-types.mts';
|
|
12
|
+
export { responseStatusCodesForContract, routeShape, type QueryOperationContract, type RegisteredRoute, type RequestContract, type ResponseContract, } from './operation-types.mts';
|
|
13
|
+
export { registerOpenApiErrorResponse } from './openapi-error-response.mts';
|
|
14
|
+
export { canonicalTemplatesByShape, catalogResponse, operationId, pathParameters, } from './openapi-route-helpers.mts';
|
|
15
|
+
export type { OpenApiDocument, OpenApiOperation, OpenApiParameter, OpenApiRequestBody, OpenApiResponse, OpenApiSchema, } from './openapi-types.mts';
|
|
16
|
+
export type { OpenApiQueryContract, OpenApiQueryParameter } from './query-types.mts';
|
|
17
|
+
export { objectNode, requiredProperty } from './schema-node-builders.mts';
|
|
18
|
+
export { writeOpenApi } from './write-openapi.mts';
|
|
19
|
+
export type { WriteOpenApiOptions } from './write-openapi.mts';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { buildOpenApiDocument } from './build-openapi-document.mjs';
|
|
2
|
+
export { queryParameters } from './build-openapi-query.mjs';
|
|
3
|
+
export { buildOperationRequestBody } from './build-openapi-request.mjs';
|
|
4
|
+
export { buildOperationResponse } from './build-openapi-response.mjs';
|
|
5
|
+
export { createComponentRegistry, sanitizeComponentName } from './component-registry.mjs';
|
|
6
|
+
export { canonicalContractSchema, hashContractSchema } from './contract-schema-canonical.mjs';
|
|
7
|
+
export { nodeToOpenApi } from './contract-schema-to-openapi.mjs';
|
|
8
|
+
export { responseStatusCodesForContract, routeShape, } from './operation-types.mjs';
|
|
9
|
+
export { registerOpenApiErrorResponse } from './openapi-error-response.mjs';
|
|
10
|
+
export { canonicalTemplatesByShape, catalogResponse, operationId, pathParameters, } from './openapi-route-helpers.mjs';
|
|
11
|
+
export { objectNode, requiredProperty } from './schema-node-builders.mjs';
|
|
12
|
+
export { writeOpenApi } from './write-openapi.mjs';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const ERROR_BODY_NODE = {
|
|
2
|
+
type: 'object',
|
|
3
|
+
properties: {
|
|
4
|
+
message: { required: true, schema: { type: 'string' } },
|
|
5
|
+
code: { required: false, schema: { type: 'string' } },
|
|
6
|
+
request_id: { required: false, schema: { type: 'string' } },
|
|
7
|
+
stack: { required: false, schema: { type: 'string' } },
|
|
8
|
+
},
|
|
9
|
+
additionalProperties: false,
|
|
10
|
+
};
|
|
11
|
+
export function registerOpenApiErrorResponse(registry) {
|
|
12
|
+
registry.register('openapi-error-response', { ErrorBody: ERROR_BODY_NODE });
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OpenApiParameter, OpenApiResponseOrRef } from './openapi-types.mts';
|
|
2
|
+
export declare function canonicalTemplatesByShape(routeTemplates: readonly string[]): Map<string, string>;
|
|
3
|
+
export declare function pathParameters(routeTemplate: string): OpenApiParameter[];
|
|
4
|
+
export declare function operationId(method: string, routeTemplate: string): string;
|
|
5
|
+
export declare function catalogResponse(kind: 'ordinary' | 'sse' | 'error-only' | 'fixed-no-content', fixedStatus?: number): {
|
|
6
|
+
responses: Record<number, OpenApiResponseOrRef>;
|
|
7
|
+
unavailable: boolean;
|
|
8
|
+
unavailableReason?: string;
|
|
9
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { STATUS_CODES } from 'node:http';
|
|
2
|
+
import { routeShape } from './operation-types.mjs';
|
|
3
|
+
export function canonicalTemplatesByShape(routeTemplates) {
|
|
4
|
+
const canonical = new Map();
|
|
5
|
+
for (const routeTemplate of routeTemplates) {
|
|
6
|
+
const shape = routeShape(routeTemplate);
|
|
7
|
+
const existing = canonical.get(shape);
|
|
8
|
+
if (!existing || routeTemplate.localeCompare(existing) < 0)
|
|
9
|
+
canonical.set(shape, routeTemplate);
|
|
10
|
+
}
|
|
11
|
+
return canonical;
|
|
12
|
+
}
|
|
13
|
+
export function pathParameters(routeTemplate) {
|
|
14
|
+
return [...routeTemplate.matchAll(/:(\w+)/g)].map((match) => ({
|
|
15
|
+
name: match[1],
|
|
16
|
+
in: 'path',
|
|
17
|
+
required: true,
|
|
18
|
+
schema: { type: 'string' },
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
export function operationId(method, routeTemplate) {
|
|
22
|
+
const path = routeTemplate.replace(/^\//, '').replace(/[/:]+/g, '_');
|
|
23
|
+
return `${method.toLowerCase()}_${path}`;
|
|
24
|
+
}
|
|
25
|
+
export function catalogResponse(kind, fixedStatus) {
|
|
26
|
+
if (kind === 'fixed-no-content') {
|
|
27
|
+
if (fixedStatus === undefined)
|
|
28
|
+
throw new Error('fixed-no-content route requires a status');
|
|
29
|
+
return {
|
|
30
|
+
responses: {
|
|
31
|
+
[fixedStatus]: { description: STATUS_CODES[fixedStatus] ?? 'Response' },
|
|
32
|
+
},
|
|
33
|
+
unavailable: false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (kind === 'error-only') {
|
|
37
|
+
const reason = 'registered route has no success response';
|
|
38
|
+
return {
|
|
39
|
+
responses: { 405: { $ref: '#/components/responses/Error' } },
|
|
40
|
+
unavailable: true,
|
|
41
|
+
unavailableReason: reason,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const reason = kind === 'sse'
|
|
45
|
+
? 'SSE event payload sequence is not statically extracted'
|
|
46
|
+
: 'registered route has no statically recognized response emission';
|
|
47
|
+
return {
|
|
48
|
+
responses: kind === 'sse'
|
|
49
|
+
? {
|
|
50
|
+
200: {
|
|
51
|
+
description: 'OK',
|
|
52
|
+
content: { 'text/event-stream': { schema: {} } },
|
|
53
|
+
'x-schema-unavailable': true,
|
|
54
|
+
'x-schema-unavailable-reason': reason,
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
: {},
|
|
58
|
+
unavailable: true,
|
|
59
|
+
unavailableReason: reason,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export type OpenApiSchema = {
|
|
2
|
+
$ref?: string;
|
|
3
|
+
type?: 'null' | 'boolean' | 'integer' | 'number' | 'string' | 'array' | 'object';
|
|
4
|
+
format?: 'uri' | 'uuid';
|
|
5
|
+
const?: boolean | number | string;
|
|
6
|
+
enum?: (boolean | number | string)[];
|
|
7
|
+
items?: OpenApiSchema | false;
|
|
8
|
+
prefixItems?: OpenApiSchema[];
|
|
9
|
+
minItems?: number;
|
|
10
|
+
maxItems?: number;
|
|
11
|
+
minimum?: number;
|
|
12
|
+
maximum?: number;
|
|
13
|
+
pattern?: string;
|
|
14
|
+
default?: boolean | number | string;
|
|
15
|
+
properties?: Record<string, OpenApiSchema>;
|
|
16
|
+
required?: string[];
|
|
17
|
+
additionalProperties?: false | OpenApiSchema;
|
|
18
|
+
anyOf?: OpenApiSchema[];
|
|
19
|
+
allOf?: OpenApiSchema[];
|
|
20
|
+
};
|
|
21
|
+
export type OpenApiParameter = {
|
|
22
|
+
name: string;
|
|
23
|
+
in: 'path';
|
|
24
|
+
required: true;
|
|
25
|
+
schema: {
|
|
26
|
+
type: 'string';
|
|
27
|
+
};
|
|
28
|
+
} | {
|
|
29
|
+
name: string;
|
|
30
|
+
in: 'query';
|
|
31
|
+
required: false;
|
|
32
|
+
description?: string;
|
|
33
|
+
style?: 'form';
|
|
34
|
+
explode?: false;
|
|
35
|
+
schema: OpenApiSchema;
|
|
36
|
+
};
|
|
37
|
+
export type OpenApiResponse = {
|
|
38
|
+
description: string;
|
|
39
|
+
content?: Record<string, {
|
|
40
|
+
schema: OpenApiSchema;
|
|
41
|
+
}>;
|
|
42
|
+
'x-schema-unavailable'?: true;
|
|
43
|
+
'x-schema-unavailable-reason'?: string;
|
|
44
|
+
};
|
|
45
|
+
export type OpenApiResponseOrRef = OpenApiResponse | {
|
|
46
|
+
$ref: string;
|
|
47
|
+
};
|
|
48
|
+
export type OpenApiRequestBody = {
|
|
49
|
+
content: {
|
|
50
|
+
'application/json': {
|
|
51
|
+
schema: OpenApiSchema;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
'x-request-schema-unavailable'?: true;
|
|
55
|
+
'x-request-schema-unavailable-reason'?: string;
|
|
56
|
+
};
|
|
57
|
+
export type OpenApiOperation = {
|
|
58
|
+
operationId: string;
|
|
59
|
+
parameters?: OpenApiParameter[];
|
|
60
|
+
requestBody?: OpenApiRequestBody;
|
|
61
|
+
responses: Record<string, OpenApiResponseOrRef>;
|
|
62
|
+
'x-schema-unavailable'?: true;
|
|
63
|
+
'x-schema-unavailable-reason'?: string;
|
|
64
|
+
};
|
|
65
|
+
export type OpenApiDocument = {
|
|
66
|
+
openapi: '3.1.0';
|
|
67
|
+
info: {
|
|
68
|
+
title: string;
|
|
69
|
+
version: string;
|
|
70
|
+
};
|
|
71
|
+
paths: Record<string, Record<string, OpenApiOperation>>;
|
|
72
|
+
components: {
|
|
73
|
+
schemas: Record<string, OpenApiSchema>;
|
|
74
|
+
responses: {
|
|
75
|
+
Error: OpenApiResponse;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
'x-unavailable-routes': string[];
|
|
79
|
+
'x-unavailable-request-routes': string[];
|
|
80
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ContractSchema } from './contract-schema-types.mts';
|
|
2
|
+
import type { OpenApiQueryContract } from './query-types.mts';
|
|
3
|
+
export type ResponseContract = {
|
|
4
|
+
source: string;
|
|
5
|
+
schema: ContractSchema;
|
|
6
|
+
method: string;
|
|
7
|
+
routeTemplate: string;
|
|
8
|
+
statusCodes?: readonly [number, ...number[]];
|
|
9
|
+
statusKnowledge?: 'default' | 'explicit' | 'unknown';
|
|
10
|
+
bodyKind?: 'content' | 'none';
|
|
11
|
+
mediaType?: string;
|
|
12
|
+
mediaTypeKnowledge?: 'known' | 'none' | 'unknown';
|
|
13
|
+
unavailableReason?: string;
|
|
14
|
+
};
|
|
15
|
+
export type RequestContract = {
|
|
16
|
+
source: string;
|
|
17
|
+
schema: ContractSchema;
|
|
18
|
+
method: string;
|
|
19
|
+
routeTemplate: string;
|
|
20
|
+
unavailableReason?: string;
|
|
21
|
+
};
|
|
22
|
+
export type QueryOperationContract = {
|
|
23
|
+
method: string;
|
|
24
|
+
routeTemplate: string;
|
|
25
|
+
parameters: OpenApiQueryContract;
|
|
26
|
+
};
|
|
27
|
+
export type RegisteredRoute = {
|
|
28
|
+
method: string;
|
|
29
|
+
routeTemplate: string;
|
|
30
|
+
kind: 'ordinary' | 'sse' | 'error-only' | 'fixed-no-content';
|
|
31
|
+
fixedStatus?: number;
|
|
32
|
+
source: string;
|
|
33
|
+
};
|
|
34
|
+
export declare function responseStatusCodesForContract(contract: ResponseContract): number[];
|
|
35
|
+
export declare function routeShape(routeTemplate: string): string;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function responseStatusCodesForContract(contract) {
|
|
2
|
+
if (contract.statusKnowledge === 'unknown')
|
|
3
|
+
return [];
|
|
4
|
+
if (contract.statusCodes)
|
|
5
|
+
return [...new Set(contract.statusCodes)].toSorted((a, b) => a - b);
|
|
6
|
+
const bodyKind = contract.bodyKind ?? (contract.schema.root.type === 'null' ? 'none' : 'content');
|
|
7
|
+
return [bodyKind === 'none' ? 204 : 200];
|
|
8
|
+
}
|
|
9
|
+
export function routeShape(routeTemplate) {
|
|
10
|
+
return routeTemplate.replace(/:[^/]+/g, ':');
|
|
11
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type OpenApiQueryParameter = {
|
|
2
|
+
kind: 'string';
|
|
3
|
+
format?: 'uuid' | 'uri';
|
|
4
|
+
description?: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'uuid-or-uri';
|
|
7
|
+
description?: string;
|
|
8
|
+
} | {
|
|
9
|
+
kind: 'boolean';
|
|
10
|
+
description?: string;
|
|
11
|
+
} | {
|
|
12
|
+
kind: 'nullable-boolean';
|
|
13
|
+
description?: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'number';
|
|
16
|
+
description?: string;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'integer';
|
|
19
|
+
minimum: number;
|
|
20
|
+
maximum: number;
|
|
21
|
+
default?: number;
|
|
22
|
+
description?: string;
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'enum';
|
|
25
|
+
values: readonly string[];
|
|
26
|
+
default?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'csv-array';
|
|
30
|
+
items: OpenApiQueryParameter;
|
|
31
|
+
style: 'form';
|
|
32
|
+
explode: false;
|
|
33
|
+
description?: string;
|
|
34
|
+
};
|
|
35
|
+
export type OpenApiQueryContract = Readonly<Record<string, OpenApiQueryParameter>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ContractSchemaNode, ContractSchemaProperty } from './contract-schema-types.mts';
|
|
2
|
+
export declare function objectNode(properties?: Record<string, ContractSchemaProperty>): ContractSchemaNode;
|
|
3
|
+
export declare function requiredProperty(schema: ContractSchemaNode, required?: boolean): ContractSchemaProperty;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { OpenApiDocument } from './openapi-types.mts';
|
|
2
|
+
export type WriteOpenApiOptions = {
|
|
3
|
+
path: string;
|
|
4
|
+
document: OpenApiDocument;
|
|
5
|
+
check?: boolean;
|
|
6
|
+
format?: (path: string, raw: string) => Promise<string>;
|
|
7
|
+
stringify?: (value: unknown) => string;
|
|
8
|
+
};
|
|
9
|
+
export declare function writeOpenApi(options: WriteOpenApiOptions): Promise<void>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
function defaultStringify(value) {
|
|
4
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
5
|
+
}
|
|
6
|
+
export async function writeOpenApi(options) {
|
|
7
|
+
const stringify = options.stringify ?? defaultStringify;
|
|
8
|
+
const format = options.format ?? (async (_path, raw) => raw);
|
|
9
|
+
const content = await format(options.path, stringify(options.document));
|
|
10
|
+
if (options.check) {
|
|
11
|
+
const actual = await readFile(options.path, 'utf8').catch((error) => error.code === 'ENOENT' ? null : Promise.reject(error));
|
|
12
|
+
if (actual !== content) {
|
|
13
|
+
throw new Error(`${options.path} is stale. Regenerate the OpenAPI document and commit it.`);
|
|
14
|
+
}
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
await mkdir(dirname(options.path), { recursive: true });
|
|
18
|
+
await writeFile(options.path, content);
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SchemaCatalog } from './catalog-queries.mts';
|
|
2
|
+
import type { SchemaGrowthMaps, SchemaSnapshot } from './types.mts';
|
|
3
|
+
export { buildTableSnapshots } from './table-snapshot.mts';
|
|
4
|
+
/**
|
|
5
|
+
* Transforms raw pg_catalog rows into the committed, name-keyed schema snapshot object. Pure —
|
|
6
|
+
* no I/O. Table-specific catalog validation and structured facts live in table-snapshot.mts;
|
|
7
|
+
* this module owns the remaining schema-wide object groups and final deterministic assembly.
|
|
8
|
+
*/
|
|
9
|
+
export declare function buildSchemaSnapshot(catalog: SchemaCatalog, growth: SchemaGrowthMaps): SchemaSnapshot;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { groupBy, keyed } from './snapshot-build-utils.mjs';
|
|
2
|
+
import { buildTableSnapshots } from './table-snapshot.mjs';
|
|
3
|
+
export { buildTableSnapshots } from './table-snapshot.mjs';
|
|
4
|
+
/**
|
|
5
|
+
* Transforms raw pg_catalog rows into the committed, name-keyed schema snapshot object. Pure —
|
|
6
|
+
* no I/O. Table-specific catalog validation and structured facts live in table-snapshot.mts;
|
|
7
|
+
* this module owns the remaining schema-wide object groups and final deterministic assembly.
|
|
8
|
+
*/
|
|
9
|
+
export function buildSchemaSnapshot(catalog, growth) {
|
|
10
|
+
const tables = buildTableSnapshots({
|
|
11
|
+
catalog,
|
|
12
|
+
columnsByTable: groupBy(catalog.columns, (row) => row.table_name),
|
|
13
|
+
constraintsByTable: groupBy(catalog.constraints, (row) => row.table_name),
|
|
14
|
+
indexesByTable: groupBy(catalog.indexes, (row) => row.table_name),
|
|
15
|
+
triggersByTable: groupBy(catalog.triggers, (row) => row.table_name),
|
|
16
|
+
growth,
|
|
17
|
+
});
|
|
18
|
+
const enumValuesByName = groupBy(catalog.enums, (row) => row.enum_name);
|
|
19
|
+
const views = {};
|
|
20
|
+
for (const view of catalog.views) {
|
|
21
|
+
views[view.view_name] = {
|
|
22
|
+
definition: view.definition,
|
|
23
|
+
comment: view.comment,
|
|
24
|
+
materialized: view.materialized,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const enums = {};
|
|
28
|
+
for (const [enumName, values] of enumValuesByName) {
|
|
29
|
+
enums[enumName] = { values: values.map((value) => value.value) };
|
|
30
|
+
}
|
|
31
|
+
const extensions = {};
|
|
32
|
+
for (const extension of catalog.extensions) {
|
|
33
|
+
extensions[extension.extension_name] = { version: extension.version };
|
|
34
|
+
}
|
|
35
|
+
const functions = {};
|
|
36
|
+
for (const fn of catalog.functions) {
|
|
37
|
+
const key = fn.identity_arguments
|
|
38
|
+
? `${fn.function_name}(${fn.identity_arguments})`
|
|
39
|
+
: fn.function_name;
|
|
40
|
+
functions[key] = { definition: fn.definition };
|
|
41
|
+
}
|
|
42
|
+
const policies = {};
|
|
43
|
+
for (const policy of catalog.policies) {
|
|
44
|
+
policies[`${policy.table_name}.${policy.policy_name}`] = {
|
|
45
|
+
table: policy.table_name,
|
|
46
|
+
command: policy.command,
|
|
47
|
+
pgRoles: policy.pg_roles,
|
|
48
|
+
using: policy.using_expression,
|
|
49
|
+
withCheck: policy.with_check_expression,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
formatVersion: 2,
|
|
54
|
+
tables,
|
|
55
|
+
views: keyed(Object.entries(views)),
|
|
56
|
+
enums: keyed(Object.entries(enums)),
|
|
57
|
+
extensions: keyed(Object.entries(extensions)),
|
|
58
|
+
functions: keyed(Object.entries(functions)),
|
|
59
|
+
policies: keyed(Object.entries(policies)),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CatalogQuery } from './types.mts';
|
|
2
|
+
export type CatalogEnumValueRow = {
|
|
3
|
+
enum_name: string;
|
|
4
|
+
value: string;
|
|
5
|
+
sort_order: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function readEnums(query: CatalogQuery): Promise<CatalogEnumValueRow[]>;
|
|
8
|
+
export type CatalogViewRow = {
|
|
9
|
+
view_name: string;
|
|
10
|
+
materialized: boolean;
|
|
11
|
+
definition: string;
|
|
12
|
+
comment: string | null;
|
|
13
|
+
};
|
|
14
|
+
export declare function readViews(query: CatalogQuery): Promise<CatalogViewRow[]>;
|
|
15
|
+
export type CatalogExtensionRow = {
|
|
16
|
+
extension_name: string;
|
|
17
|
+
version: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function readExtensions(query: CatalogQuery): Promise<CatalogExtensionRow[]>;
|
|
20
|
+
export type CatalogFunctionRow = {
|
|
21
|
+
function_name: string;
|
|
22
|
+
identity_arguments: string;
|
|
23
|
+
definition: string;
|
|
24
|
+
};
|
|
25
|
+
export declare function readFunctions(query: CatalogQuery): Promise<CatalogFunctionRow[]>;
|
|
26
|
+
export type CatalogPolicyRow = {
|
|
27
|
+
table_name: string;
|
|
28
|
+
policy_name: string;
|
|
29
|
+
command: string;
|
|
30
|
+
pg_roles: string[];
|
|
31
|
+
using_expression: string | null;
|
|
32
|
+
with_check_expression: string | null;
|
|
33
|
+
};
|
|
34
|
+
export declare function readPolicies(query: CatalogQuery): Promise<CatalogPolicyRow[]>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { EXCLUDE_EXTENSION_OWNED } from './catalog-shared.mjs';
|
|
2
|
+
export async function readEnums(query) {
|
|
3
|
+
const { rows } = await query(`/* readSchemaSnapshotEnums */
|
|
4
|
+
SELECT
|
|
5
|
+
target.typname AS enum_name,
|
|
6
|
+
pg_enum.enumlabel AS value,
|
|
7
|
+
pg_enum.enumsortorder AS sort_order
|
|
8
|
+
FROM pg_enum
|
|
9
|
+
JOIN pg_type target ON target.oid = pg_enum.enumtypid
|
|
10
|
+
JOIN pg_namespace ON pg_namespace.oid = target.typnamespace
|
|
11
|
+
WHERE pg_namespace.nspname = 'public'
|
|
12
|
+
AND ${EXCLUDE_EXTENSION_OWNED}
|
|
13
|
+
ORDER BY enum_name, sort_order`);
|
|
14
|
+
return rows;
|
|
15
|
+
}
|
|
16
|
+
export async function readViews(query) {
|
|
17
|
+
const { rows } = await query(`/* readSchemaSnapshotViews */
|
|
18
|
+
SELECT
|
|
19
|
+
target.relname AS view_name,
|
|
20
|
+
target.relkind = 'm' AS materialized,
|
|
21
|
+
pg_get_viewdef(target.oid) AS definition,
|
|
22
|
+
obj_description(target.oid, 'pg_class') AS comment
|
|
23
|
+
FROM pg_class target
|
|
24
|
+
JOIN pg_namespace ON pg_namespace.oid = target.relnamespace
|
|
25
|
+
WHERE pg_namespace.nspname = 'public'
|
|
26
|
+
AND target.relkind = ANY($1)
|
|
27
|
+
AND ${EXCLUDE_EXTENSION_OWNED}
|
|
28
|
+
ORDER BY view_name`, [['v', 'm']]);
|
|
29
|
+
return rows;
|
|
30
|
+
}
|
|
31
|
+
export async function readExtensions(query) {
|
|
32
|
+
const { rows } = await query(`/* readSchemaSnapshotExtensions */
|
|
33
|
+
SELECT extname AS extension_name, extversion AS version
|
|
34
|
+
FROM pg_extension
|
|
35
|
+
ORDER BY extension_name`);
|
|
36
|
+
return rows;
|
|
37
|
+
}
|
|
38
|
+
export async function readFunctions(query) {
|
|
39
|
+
const { rows } = await query(`/* readSchemaSnapshotFunctions */
|
|
40
|
+
SELECT
|
|
41
|
+
target.proname AS function_name,
|
|
42
|
+
pg_get_function_identity_arguments(target.oid) AS identity_arguments,
|
|
43
|
+
pg_get_functiondef(target.oid) AS definition
|
|
44
|
+
FROM pg_proc target
|
|
45
|
+
JOIN pg_namespace ON pg_namespace.oid = target.pronamespace
|
|
46
|
+
WHERE pg_namespace.nspname = 'public'
|
|
47
|
+
AND ${EXCLUDE_EXTENSION_OWNED}
|
|
48
|
+
ORDER BY function_name, identity_arguments`);
|
|
49
|
+
return rows;
|
|
50
|
+
}
|
|
51
|
+
export async function readPolicies(query) {
|
|
52
|
+
const { rows } = await query(`/* readSchemaSnapshotPolicies */
|
|
53
|
+
SELECT
|
|
54
|
+
tablename AS table_name,
|
|
55
|
+
policyname AS policy_name,
|
|
56
|
+
cmd AS command,
|
|
57
|
+
roles AS pg_roles,
|
|
58
|
+
qual AS using_expression,
|
|
59
|
+
with_check AS with_check_expression
|
|
60
|
+
FROM pg_policies
|
|
61
|
+
WHERE schemaname = 'public'
|
|
62
|
+
ORDER BY table_name, policy_name`);
|
|
63
|
+
return rows;
|
|
64
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type CatalogEnumValueRow, type CatalogExtensionRow, type CatalogFunctionRow, type CatalogPolicyRow, type CatalogViewRow, readEnums, readExtensions, readFunctions, readPolicies, readViews } from './catalog-objects.mts';
|
|
2
|
+
import { type CatalogConstraintRow, readConstraints } from './catalog-table-constraints.mts';
|
|
3
|
+
import { type CatalogIndexRow, type CatalogTriggerRow, readIndexes, readTriggers } from './catalog-table-indexes.mts';
|
|
4
|
+
import { type CatalogColumnRow, type CatalogTableRow, readColumns, readTables } from './catalog-tables.mts';
|
|
5
|
+
import type { CatalogQuery } from './types.mts';
|
|
6
|
+
export { readColumns, readConstraints, readEnums, readExtensions, readFunctions, readIndexes, readPolicies, readTables, readTriggers, readViews, };
|
|
7
|
+
export type { CatalogColumnRow, CatalogConstraintRow, CatalogEnumValueRow, CatalogExtensionRow, CatalogFunctionRow, CatalogIndexRow, CatalogPolicyRow, CatalogTableRow, CatalogTriggerRow, CatalogViewRow, };
|
|
8
|
+
export type SchemaCatalog = {
|
|
9
|
+
tables: CatalogTableRow[];
|
|
10
|
+
columns: CatalogColumnRow[];
|
|
11
|
+
constraints: CatalogConstraintRow[];
|
|
12
|
+
indexes: CatalogIndexRow[];
|
|
13
|
+
triggers: CatalogTriggerRow[];
|
|
14
|
+
enums: CatalogEnumValueRow[];
|
|
15
|
+
views: CatalogViewRow[];
|
|
16
|
+
extensions: CatalogExtensionRow[];
|
|
17
|
+
functions: CatalogFunctionRow[];
|
|
18
|
+
policies: CatalogPolicyRow[];
|
|
19
|
+
};
|
|
20
|
+
export declare function readSchemaCatalog(query: CatalogQuery): Promise<SchemaCatalog>;
|