@kozou/api 0.0.1 → 0.2.1
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/LICENSE +202 -0
- package/README.md +93 -1
- package/dist/auth.d.ts +75 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +128 -0
- package/dist/auth.js.map +1 -0
- package/dist/embed.d.ts +46 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +181 -0
- package/dist/embed.js.map +1 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +33 -0
- package/dist/errors.js.map +1 -0
- package/dist/handler.d.ts +35 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +209 -0
- package/dist/handler.js.map +1 -0
- package/dist/ident.d.ts +7 -0
- package/dist/ident.d.ts.map +1 -0
- package/dist/ident.js +12 -0
- package/dist/ident.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/openapi.d.ts +9 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +260 -0
- package/dist/openapi.js.map +1 -0
- package/dist/query-builder.d.ts +59 -0
- package/dist/query-builder.d.ts.map +1 -0
- package/dist/query-builder.js +193 -0
- package/dist/query-builder.js.map +1 -0
- package/dist/schema-lookup.d.ts +30 -0
- package/dist/schema-lookup.d.ts.map +1 -0
- package/dist/schema-lookup.js +61 -0
- package/dist/schema-lookup.js.map +1 -0
- package/dist/startApiServer.d.ts +53 -0
- package/dist/startApiServer.d.ts.map +1 -0
- package/dist/startApiServer.js +210 -0
- package/dist/startApiServer.js.map +1 -0
- package/package.json +44 -4
package/dist/embed.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// The `embed` sub-language for the read path. Inlines related rows as nested
|
|
2
|
+
// JSON, to a capped depth, in a single SQL statement:
|
|
3
|
+
// - forward (to-one / one-to-one): the parent's FK -> a single object.
|
|
4
|
+
// - reverse (one-to-many): children whose FK points back -> an array.
|
|
5
|
+
//
|
|
6
|
+
// Pipeline:
|
|
7
|
+
// parseEmbedParam "a,b.c" -> string[][] (pure syntax)
|
|
8
|
+
// resolveEmbedSpec paths + lookup -> EmbedNode[] (validate; all 400s)
|
|
9
|
+
// buildEmbedSelectFragment spec -> SQL SELECT-list text (identifiers only)
|
|
10
|
+
//
|
|
11
|
+
// Safety: every identifier emitted comes from the introspected schema (a
|
|
12
|
+
// relation's field / referenced column, or a target's declared columns), never
|
|
13
|
+
// from the raw request string. The selector only *chooses* which allowlisted
|
|
14
|
+
// relation to follow. No bound parameters are produced, so the caller's $n
|
|
15
|
+
// numbering is untouched.
|
|
16
|
+
import { badRequest } from './errors.js';
|
|
17
|
+
import { quoteIdent, qualified } from './ident.js';
|
|
18
|
+
/** Maximum relation chain length (dot-separated segments) per embed path. */
|
|
19
|
+
export const MAX_EMBED_DEPTH = 5;
|
|
20
|
+
/** Maximum number of distinct relations a single request may embed. */
|
|
21
|
+
export const MAX_EMBED_RELATIONS = 25;
|
|
22
|
+
/** Maximum number of child rows inlined per parent for a to-many embed. */
|
|
23
|
+
export const MAX_EMBED_CHILDREN = 100;
|
|
24
|
+
/** Split a raw `embed` value into paths. Pure: no schema knowledge.
|
|
25
|
+
* `"author,editions.books"` -> `[["author"], ["editions", "books"]]`. */
|
|
26
|
+
export function parseEmbedParam(raw) {
|
|
27
|
+
if (raw === null || raw === undefined)
|
|
28
|
+
return [];
|
|
29
|
+
const paths = [];
|
|
30
|
+
for (const group of raw.split(',')) {
|
|
31
|
+
const segments = group
|
|
32
|
+
.split('.')
|
|
33
|
+
.map((s) => s.trim())
|
|
34
|
+
.filter((s) => s.length > 0);
|
|
35
|
+
if (segments.length > 0)
|
|
36
|
+
paths.push(segments);
|
|
37
|
+
}
|
|
38
|
+
return paths;
|
|
39
|
+
}
|
|
40
|
+
/** Resolve + validate parsed paths into an embed forest. Throws `badRequest`
|
|
41
|
+
* on any unknown / ambiguous selector, over-cap depth or count, or
|
|
42
|
+
* unembeddable target. Paths sharing a prefix are merged so a relation is
|
|
43
|
+
* embedded at most once per parent. */
|
|
44
|
+
export function resolveEmbedSpec(root, paths, lookup) {
|
|
45
|
+
const counter = { n: 0 };
|
|
46
|
+
const forest = [];
|
|
47
|
+
for (const path of paths) {
|
|
48
|
+
if (path.length > MAX_EMBED_DEPTH) {
|
|
49
|
+
throw badRequest(`Embed depth ${path.length} exceeds the maximum of ${MAX_EMBED_DEPTH}.`);
|
|
50
|
+
}
|
|
51
|
+
insertPath(root, path, 0, forest, lookup, counter);
|
|
52
|
+
}
|
|
53
|
+
return forest;
|
|
54
|
+
}
|
|
55
|
+
function insertPath(parent, path, index, siblings, lookup, counter) {
|
|
56
|
+
if (index >= path.length)
|
|
57
|
+
return;
|
|
58
|
+
const resolved = resolveSegment(parent, path[index], lookup);
|
|
59
|
+
let node = siblings.find((s) => s.kind === resolved.kind &&
|
|
60
|
+
s.target.qualifiedName === resolved.target.qualifiedName &&
|
|
61
|
+
s.relation.field === resolved.relation.field);
|
|
62
|
+
if (node === undefined) {
|
|
63
|
+
if (counter.n >= MAX_EMBED_RELATIONS) {
|
|
64
|
+
throw badRequest(`Embed requests too many relations (max ${MAX_EMBED_RELATIONS}).`);
|
|
65
|
+
}
|
|
66
|
+
const key = chooseKey(resolved.target, resolved.relation, siblings, parent);
|
|
67
|
+
node = { kind: resolved.kind, relation: resolved.relation, target: resolved.target, key, children: [] };
|
|
68
|
+
siblings.push(node);
|
|
69
|
+
counter.n += 1;
|
|
70
|
+
}
|
|
71
|
+
insertPath(node.target, path, index + 1, node.children, lookup, counter);
|
|
72
|
+
}
|
|
73
|
+
/** Resolve one selector against a parent: a forward relation first, then a
|
|
74
|
+
* reverse (child) relation. Views expose neither. */
|
|
75
|
+
function resolveSegment(parent, selector, lookup) {
|
|
76
|
+
if (parent.kind === 'view') {
|
|
77
|
+
throw badRequest(`Resource "${parent.name}" is a view and exposes no embeddable relations.`);
|
|
78
|
+
}
|
|
79
|
+
const forward = matchForward(parent, selector);
|
|
80
|
+
if (forward !== undefined) {
|
|
81
|
+
return { kind: 'to-one', relation: forward, target: resolveTarget(forward, lookup) };
|
|
82
|
+
}
|
|
83
|
+
const reverse = matchReverse(parent, selector, lookup);
|
|
84
|
+
if (reverse !== undefined) {
|
|
85
|
+
return { kind: 'to-many', relation: reverse.relation, target: reverse.child };
|
|
86
|
+
}
|
|
87
|
+
throw badRequest(`Unknown embed relation "${selector}" on resource "${parent.name}".`);
|
|
88
|
+
}
|
|
89
|
+
/** Match a forward to-one relation by FK field name, or by referenced table
|
|
90
|
+
* name when exactly one FK targets it. */
|
|
91
|
+
function matchForward(parent, selector) {
|
|
92
|
+
const byField = parent.relations.find((r) => r.field === selector);
|
|
93
|
+
if (byField !== undefined)
|
|
94
|
+
return byField;
|
|
95
|
+
const byTable = parent.relations.filter((r) => r.references.table === selector);
|
|
96
|
+
if (byTable.length === 1)
|
|
97
|
+
return byTable[0];
|
|
98
|
+
if (byTable.length > 1) {
|
|
99
|
+
const fields = byTable.map((r) => r.field).join('", "');
|
|
100
|
+
throw badRequest(`Ambiguous embed "${selector}" on "${parent.name}": foreign keys "${fields}" all reference "${selector}"; use the foreign-key column name.`);
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
/** Match a reverse to-many relation by child table name, when exactly one of
|
|
105
|
+
* that child's foreign keys references the parent. */
|
|
106
|
+
function matchReverse(parent, selector, lookup) {
|
|
107
|
+
const candidates = lookup.reverse(parent.qualifiedName).filter((e) => e.child.name === selector);
|
|
108
|
+
if (candidates.length === 1)
|
|
109
|
+
return candidates[0];
|
|
110
|
+
if (candidates.length > 1) {
|
|
111
|
+
const fields = candidates.map((e) => e.relation.field).join('", "');
|
|
112
|
+
throw badRequest(`Ambiguous reverse embed "${selector}" on "${parent.name}": "${selector}" references it via "${fields}"; embedding multiple reverse keys is not yet supported.`);
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
function resolveTarget(relation, lookup) {
|
|
117
|
+
const qn = `${relation.references.schema}.${relation.references.table}`;
|
|
118
|
+
const target = lookup.resolve(qn);
|
|
119
|
+
if (target === undefined) {
|
|
120
|
+
throw badRequest(`Embed target "${qn}" is not an available resource.`);
|
|
121
|
+
}
|
|
122
|
+
return target;
|
|
123
|
+
}
|
|
124
|
+
/** Pick a result key that collides with neither a sibling embed nor a real
|
|
125
|
+
* column on the parent (which would shadow the raw scalar value). Prefers the
|
|
126
|
+
* target table name, then the FK field with a trailing id-suffix stripped,
|
|
127
|
+
* then the raw FK field. */
|
|
128
|
+
function chooseKey(target, relation, siblings, parent) {
|
|
129
|
+
const taken = new Set(siblings.map((s) => s.key));
|
|
130
|
+
const columns = new Set(parent.columns.map((c) => c.name));
|
|
131
|
+
const candidates = [target.name, stripIdSuffix(relation.field), relation.field];
|
|
132
|
+
for (const key of candidates) {
|
|
133
|
+
if (key.length > 0 && !taken.has(key) && !columns.has(key))
|
|
134
|
+
return key;
|
|
135
|
+
}
|
|
136
|
+
throw badRequest(`Cannot derive a non-conflicting embed key for "${relation.field}" on "${parent.name}".`);
|
|
137
|
+
}
|
|
138
|
+
function stripIdSuffix(field) {
|
|
139
|
+
const stripped = field.replace(/_(id|uuid|fk|key)$/i, '');
|
|
140
|
+
return stripped.length > 0 ? stripped : field;
|
|
141
|
+
}
|
|
142
|
+
/** Render the SELECT-list fragment for an embed forest, composed recursively
|
|
143
|
+
* for depth. `parentRef` is the SQL reference for the parent row scope (the
|
|
144
|
+
* qualified table name at the top level, a generated alias below); `counter`
|
|
145
|
+
* keeps aliases unique across the whole statement. Identifiers only — no
|
|
146
|
+
* bound parameters are produced.
|
|
147
|
+
*
|
|
148
|
+
* - to-one -> `(SELECT to_jsonb(eN) FROM (...) eN) AS "key"` (object | null)
|
|
149
|
+
* - to-many -> `(SELECT coalesce(jsonb_agg(...), '[]') FROM (...) eN) AS "key"` (array) */
|
|
150
|
+
export function buildEmbedSelectFragment(spec, parentRef, counter) {
|
|
151
|
+
let out = '';
|
|
152
|
+
for (const node of spec) {
|
|
153
|
+
counter.n += 1;
|
|
154
|
+
const alias = `e${counter.n}`;
|
|
155
|
+
const cols = node.target.columns.length > 0
|
|
156
|
+
? node.target.columns.map((c) => quoteIdent(c.name)).join(', ')
|
|
157
|
+
: '*';
|
|
158
|
+
const children = buildEmbedSelectFragment(node.children, alias, counter);
|
|
159
|
+
const fkField = quoteIdent(node.relation.field);
|
|
160
|
+
const refCol = quoteIdent(node.relation.references.column);
|
|
161
|
+
if (node.kind === 'to-one') {
|
|
162
|
+
const inner = `SELECT ${cols}${children} FROM ${qualified(node.target)} ${alias} WHERE ${alias}.${refCol} = ${parentRef}.${fkField}`;
|
|
163
|
+
out += `, (SELECT to_jsonb(${alias}) FROM (${inner}) ${alias}) AS ${quoteIdent(node.key)}`;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const order = orderByPrimaryKey(node.target, alias);
|
|
167
|
+
const inner = `SELECT ${cols}${children} FROM ${qualified(node.target)} ${alias} WHERE ${alias}.${fkField} = ${parentRef}.${refCol}${order} LIMIT ${MAX_EMBED_CHILDREN}`;
|
|
168
|
+
out += `, (SELECT coalesce(jsonb_agg(to_jsonb(${alias})${order}), '[]'::jsonb) FROM (${inner}) ${alias}) AS ${quoteIdent(node.key)}`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
/** `ORDER BY <alias>.<pk>...` for deterministic, bounded to-many results.
|
|
174
|
+
* Empty when the target has no primary key. */
|
|
175
|
+
function orderByPrimaryKey(target, alias) {
|
|
176
|
+
if (target.primaryKey.length === 0)
|
|
177
|
+
return '';
|
|
178
|
+
const cols = target.primaryKey.map((c) => `${alias}.${quoteIdent(c)}`).join(', ');
|
|
179
|
+
return ` ORDER BY ${cols}`;
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=embed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embed.js","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare class KozouApiError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly code: string;
|
|
4
|
+
constructor(status: number, code: string, message: string);
|
|
5
|
+
}
|
|
6
|
+
/** Shape of the JSON body returned for any non-2xx response. */
|
|
7
|
+
export type ApiErrorBody = {
|
|
8
|
+
error: {
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export declare function errorBody(code: string, message: string): ApiErrorBody;
|
|
14
|
+
export declare function notFound(message: string): KozouApiError;
|
|
15
|
+
export declare function badRequest(message: string): KozouApiError;
|
|
16
|
+
export declare function methodNotAllowed(message: string): KozouApiError;
|
|
17
|
+
export declare function unauthorized(message: string): KozouApiError;
|
|
18
|
+
export declare function forbidden(message: string): KozouApiError;
|
|
19
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAKA,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAM1D;AAED,gEAAgE;AAChE,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH,CAAC;AAEF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,YAAY,CAErE;AAED,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAEvD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAEzD;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAE/D;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAE3D;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAExD"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Error type for the Kozou REST layer. A KozouApiError carries the HTTP
|
|
2
|
+
// status and a short machine-readable code; the request handler maps it
|
|
3
|
+
// to a JSON error body. Anything thrown that is *not* a KozouApiError is
|
|
4
|
+
// treated as an unexpected 500.
|
|
5
|
+
export class KozouApiError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
code;
|
|
8
|
+
constructor(status, code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'KozouApiError';
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function errorBody(code, message) {
|
|
16
|
+
return { error: { code, message } };
|
|
17
|
+
}
|
|
18
|
+
export function notFound(message) {
|
|
19
|
+
return new KozouApiError(404, 'not_found', message);
|
|
20
|
+
}
|
|
21
|
+
export function badRequest(message) {
|
|
22
|
+
return new KozouApiError(400, 'bad_request', message);
|
|
23
|
+
}
|
|
24
|
+
export function methodNotAllowed(message) {
|
|
25
|
+
return new KozouApiError(405, 'method_not_allowed', message);
|
|
26
|
+
}
|
|
27
|
+
export function unauthorized(message) {
|
|
28
|
+
return new KozouApiError(401, 'unauthorized', message);
|
|
29
|
+
}
|
|
30
|
+
export function forbidden(message) {
|
|
31
|
+
return new KozouApiError(403, 'forbidden', message);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,wEAAwE;AACxE,yEAAyE;AACzE,gCAAgC;AAEhC,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,MAAM,CAAS;IACf,IAAI,CAAS;IAEtB,YAAY,MAAc,EAAE,IAAY,EAAE,OAAe;QACvD,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAUD,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,OAAe;IACrD,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,OAAO,IAAI,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,OAAO,IAAI,aAAa,CAAC,GAAG,EAAE,aAAa,EAAE,OAAO,CAAC,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,OAAO,IAAI,aAAa,CAAC,GAAG,EAAE,oBAAoB,EAAE,OAAO,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,OAAO,IAAI,aAAa,CAAC,GAAG,EAAE,cAAc,EAAE,OAAO,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,OAAe;IACvC,OAAO,IAAI,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;AACtD,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ResourceLookup } from './schema-lookup.js';
|
|
2
|
+
import { type ListQueryParams } from './query-builder.js';
|
|
3
|
+
/** Minimal query interface satisfied by both `pg.Pool` and `pg.Client`. */
|
|
4
|
+
export type Queryable = {
|
|
5
|
+
query<R extends Record<string, unknown>>(text: string, values?: unknown[]): Promise<{
|
|
6
|
+
rows: R[];
|
|
7
|
+
rowCount: number | null;
|
|
8
|
+
}>;
|
|
9
|
+
};
|
|
10
|
+
export type ApiHandlerDeps = {
|
|
11
|
+
db: Queryable;
|
|
12
|
+
lookup: ResourceLookup;
|
|
13
|
+
/** Advertised in `GET /`. Optional; defaults to null. */
|
|
14
|
+
version?: string;
|
|
15
|
+
/** Prebuilt OpenAPI document served at `GET /openapi.json`. */
|
|
16
|
+
openapi?: Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
export type ApiHttpRequest = {
|
|
19
|
+
method: string;
|
|
20
|
+
/** URL-decoded path segments with empty segments removed. */
|
|
21
|
+
segments: string[];
|
|
22
|
+
query: URLSearchParams;
|
|
23
|
+
/** Parsed JSON request body (create / update). Undefined when absent. */
|
|
24
|
+
body?: unknown;
|
|
25
|
+
/** Raw request headers (node lower-cases the keys). Used for auth; absent on
|
|
26
|
+
* the zero-auth path. */
|
|
27
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
28
|
+
};
|
|
29
|
+
export type ApiHttpResult = {
|
|
30
|
+
status: number;
|
|
31
|
+
body: unknown;
|
|
32
|
+
};
|
|
33
|
+
export declare function handleApiRequest(deps: ApiHandlerDeps, req: ApiHttpRequest): Promise<ApiHttpResult>;
|
|
34
|
+
export declare function parseListParams(query: URLSearchParams): ListQueryParams;
|
|
35
|
+
//# sourceMappingURL=handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAY,MAAM,oBAAoB,CAAC;AACnE,OAAO,EAOL,KAAK,eAAe,EAErB,MAAM,oBAAoB,CAAC;AAG5B,2EAA2E;AAC3E,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CACpD,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,EAAE,EAAE,SAAS,CAAC;IACd,MAAM,EAAE,cAAc,CAAC;IACvB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,eAAe,CAAC;IACvB,yEAAyE;IACzE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;8BAC0B;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;CACzD,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf,CAAC;AAIF,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,cAAc,EACpB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,aAAa,CAAC,CAUxB;AAkLD,wBAAgB,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,eAAe,CAuBvE"}
|
package/dist/handler.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Framework-agnostic request handling. Maps a parsed HTTP request to a
|
|
2
|
+
// JSON result, independent of node:http (so it can be unit-tested with a
|
|
3
|
+
// fake Queryable and driven by the node:http wiring in startApiServer.ts).
|
|
4
|
+
import { KozouApiError, badRequest, errorBody, methodNotAllowed, notFound } from './errors.js';
|
|
5
|
+
import { buildGetQuery, buildListQuery, buildInsertQuery, buildUpdateQuery, buildDeleteQuery, buildRelationOptionsQuery, } from './query-builder.js';
|
|
6
|
+
import { parseEmbedParam, resolveEmbedSpec } from './embed.js';
|
|
7
|
+
const RESERVED_PARAMS = new Set(['page', 'pageSize', 'sort', 'search', 'embed']);
|
|
8
|
+
export async function handleApiRequest(deps, req) {
|
|
9
|
+
try {
|
|
10
|
+
return await route(deps, req);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
if (err instanceof KozouApiError) {
|
|
14
|
+
return { status: err.status, body: errorBody(err.code, err.message) };
|
|
15
|
+
}
|
|
16
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17
|
+
return { status: 500, body: errorBody('internal', message) };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function route(deps, req) {
|
|
21
|
+
const { method, segments, query } = req;
|
|
22
|
+
const m = method.toUpperCase();
|
|
23
|
+
if (segments.length === 0) {
|
|
24
|
+
requireMethod(method, 'GET');
|
|
25
|
+
return {
|
|
26
|
+
status: 200,
|
|
27
|
+
body: { name: 'kozou-api', version: deps.version ?? null, resources: deps.lookup.list() },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (segments.length === 1 && segments[0] === 'openapi.json') {
|
|
31
|
+
requireMethod(method, 'GET');
|
|
32
|
+
if (!deps.openapi) {
|
|
33
|
+
throw notFound('OpenAPI document is not configured.');
|
|
34
|
+
}
|
|
35
|
+
return { status: 200, body: deps.openapi };
|
|
36
|
+
}
|
|
37
|
+
if (segments.length === 1) {
|
|
38
|
+
const resource = resolveOr404(deps.lookup, segments[0]);
|
|
39
|
+
if (m === 'GET') {
|
|
40
|
+
return query.get('as') === 'options'
|
|
41
|
+
? relationOptions(deps, resource, query)
|
|
42
|
+
: listResource(deps, resource, query);
|
|
43
|
+
}
|
|
44
|
+
if (m === 'POST')
|
|
45
|
+
return createResource(deps, resource, req.body);
|
|
46
|
+
throw methodNotAllowed(`Method ${method} not allowed on a collection; use GET or POST.`);
|
|
47
|
+
}
|
|
48
|
+
if (segments.length === 2) {
|
|
49
|
+
const resource = resolveOr404(deps.lookup, segments[0]);
|
|
50
|
+
const id = segments[1];
|
|
51
|
+
if (m === 'GET')
|
|
52
|
+
return getResource(deps, resource, id, query);
|
|
53
|
+
if (m === 'PATCH')
|
|
54
|
+
return updateResource(deps, resource, id, req.body);
|
|
55
|
+
if (m === 'DELETE')
|
|
56
|
+
return deleteResource(deps, resource, id);
|
|
57
|
+
throw methodNotAllowed(`Method ${method} not allowed on an item; use GET, PATCH, or DELETE.`);
|
|
58
|
+
}
|
|
59
|
+
throw notFound(`No route for /${segments.join('/')}.`);
|
|
60
|
+
}
|
|
61
|
+
async function listResource(deps, resource, query) {
|
|
62
|
+
const params = parseListParams(query);
|
|
63
|
+
const embed = resolveEmbedSpec(resource, parseEmbedParam(query.get('embed')), deps.lookup);
|
|
64
|
+
const built = buildListQuery(resource, { ...params, embed });
|
|
65
|
+
const [dataResult, countResult] = await Promise.all([
|
|
66
|
+
deps.db.query(built.dataText, built.dataValues),
|
|
67
|
+
deps.db.query(built.countText, built.countValues),
|
|
68
|
+
]);
|
|
69
|
+
const total = Number(countResult.rows[0]?.total ?? 0);
|
|
70
|
+
return {
|
|
71
|
+
status: 200,
|
|
72
|
+
body: { rows: dataResult.rows, total, page: built.page, pageSize: built.pageSize },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function getResource(deps, resource, id, query) {
|
|
76
|
+
const embed = resolveEmbedSpec(resource, parseEmbedParam(query.get('embed')), deps.lookup);
|
|
77
|
+
const built = buildGetQuery(resource, id, embed);
|
|
78
|
+
const result = await deps.db.query(built.text, built.values);
|
|
79
|
+
if (result.rows.length === 0) {
|
|
80
|
+
return notFoundResult(resource, id);
|
|
81
|
+
}
|
|
82
|
+
return { status: 200, body: result.rows[0] };
|
|
83
|
+
}
|
|
84
|
+
async function createResource(deps, resource, body) {
|
|
85
|
+
requireWritable(resource);
|
|
86
|
+
const built = buildInsertQuery(resource, requireObjectBody(body));
|
|
87
|
+
const result = await deps.db.query(built.text, built.values);
|
|
88
|
+
return { status: 201, body: result.rows[0] };
|
|
89
|
+
}
|
|
90
|
+
async function updateResource(deps, resource, id, body) {
|
|
91
|
+
requireWritable(resource);
|
|
92
|
+
const built = buildUpdateQuery(resource, id, requireObjectBody(body));
|
|
93
|
+
const result = await deps.db.query(built.text, built.values);
|
|
94
|
+
if (result.rows.length === 0)
|
|
95
|
+
return notFoundResult(resource, id);
|
|
96
|
+
return { status: 200, body: result.rows[0] };
|
|
97
|
+
}
|
|
98
|
+
async function deleteResource(deps, resource, id) {
|
|
99
|
+
requireWritable(resource);
|
|
100
|
+
const built = buildDeleteQuery(resource, id);
|
|
101
|
+
const result = await deps.db.query(built.text, built.values);
|
|
102
|
+
if (result.rows.length === 0)
|
|
103
|
+
return notFoundResult(resource, id);
|
|
104
|
+
return { status: 200, body: result.rows[0] };
|
|
105
|
+
}
|
|
106
|
+
async function relationOptions(deps, resource, query) {
|
|
107
|
+
const labelField = query.get('label');
|
|
108
|
+
if (labelField === null || labelField.length === 0) {
|
|
109
|
+
throw badRequest('Relation options require a "label" query parameter.');
|
|
110
|
+
}
|
|
111
|
+
const fieldsRaw = query.get('fields');
|
|
112
|
+
const searchFields = fieldsRaw
|
|
113
|
+
? fieldsRaw.split(',').map((s) => s.trim()).filter((s) => s.length > 0)
|
|
114
|
+
: [];
|
|
115
|
+
const built = buildRelationOptionsQuery(resource, {
|
|
116
|
+
labelField,
|
|
117
|
+
searchFields,
|
|
118
|
+
query: query.get('q') ?? undefined,
|
|
119
|
+
limit: parsePositiveInt(query.get('limit')),
|
|
120
|
+
});
|
|
121
|
+
const result = await deps.db.query(built.text, built.values);
|
|
122
|
+
const options = result.rows.map((row) => ({
|
|
123
|
+
id: row[built.primaryKey],
|
|
124
|
+
label: String(row[built.labelField] ?? ''),
|
|
125
|
+
}));
|
|
126
|
+
return { status: 200, body: { options } };
|
|
127
|
+
}
|
|
128
|
+
function notFoundResult(resource, id) {
|
|
129
|
+
return {
|
|
130
|
+
status: 404,
|
|
131
|
+
body: errorBody('not_found', `No "${resource.name}" row with id "${id}".`),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function requireWritable(resource) {
|
|
135
|
+
if (resource.kind === 'view') {
|
|
136
|
+
throw methodNotAllowed(`Resource "${resource.name}" is a read-only view.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function requireObjectBody(body) {
|
|
140
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
141
|
+
throw badRequest('Request body must be a JSON object.');
|
|
142
|
+
}
|
|
143
|
+
return body;
|
|
144
|
+
}
|
|
145
|
+
function resolveOr404(lookup, name) {
|
|
146
|
+
const resource = lookup.resolve(name);
|
|
147
|
+
if (!resource) {
|
|
148
|
+
throw notFound(`Unknown resource "${name}".`);
|
|
149
|
+
}
|
|
150
|
+
return resource;
|
|
151
|
+
}
|
|
152
|
+
function requireMethod(method, allowed) {
|
|
153
|
+
if (method.toUpperCase() !== allowed) {
|
|
154
|
+
throw methodNotAllowed(`Method ${method} not allowed here; use ${allowed}.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export function parseListParams(query) {
|
|
158
|
+
const params = {};
|
|
159
|
+
const page = parsePositiveInt(query.get('page'));
|
|
160
|
+
if (page !== undefined)
|
|
161
|
+
params.page = page;
|
|
162
|
+
const pageSize = parsePositiveInt(query.get('pageSize'));
|
|
163
|
+
if (pageSize !== undefined)
|
|
164
|
+
params.pageSize = pageSize;
|
|
165
|
+
const search = query.get('search');
|
|
166
|
+
if (search !== null && search.length > 0)
|
|
167
|
+
params.search = search;
|
|
168
|
+
const sort = parseSort(query.get('sort'));
|
|
169
|
+
if (sort.length > 0)
|
|
170
|
+
params.sort = sort;
|
|
171
|
+
const filters = {};
|
|
172
|
+
for (const [key, value] of query.entries()) {
|
|
173
|
+
if (RESERVED_PARAMS.has(key))
|
|
174
|
+
continue;
|
|
175
|
+
filters[key] = value; // last value wins on repeated keys
|
|
176
|
+
}
|
|
177
|
+
if (Object.keys(filters).length > 0)
|
|
178
|
+
params.filters = filters;
|
|
179
|
+
return params;
|
|
180
|
+
}
|
|
181
|
+
function parsePositiveInt(raw) {
|
|
182
|
+
if (raw === null || raw.length === 0)
|
|
183
|
+
return undefined;
|
|
184
|
+
const n = Number(raw);
|
|
185
|
+
if (!Number.isFinite(n))
|
|
186
|
+
return undefined;
|
|
187
|
+
return n;
|
|
188
|
+
}
|
|
189
|
+
function parseSort(raw) {
|
|
190
|
+
if (raw === null || raw.length === 0)
|
|
191
|
+
return [];
|
|
192
|
+
const result = [];
|
|
193
|
+
for (const token of raw.split(',')) {
|
|
194
|
+
const trimmed = token.trim();
|
|
195
|
+
if (trimmed.length === 0)
|
|
196
|
+
continue;
|
|
197
|
+
const dot = trimmed.lastIndexOf('.');
|
|
198
|
+
if (dot > 0) {
|
|
199
|
+
const suffix = trimmed.slice(dot + 1).toLowerCase();
|
|
200
|
+
if (suffix === 'asc' || suffix === 'desc') {
|
|
201
|
+
result.push({ field: trimmed.slice(0, dot), order: suffix });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
result.push({ field: trimmed, order: 'asc' });
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.js","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"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"}
|
package/dist/ident.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Resource } from './schema-lookup.js';
|
|
2
|
+
/** Quote an identifier for safe inlining (defense in depth on top of the
|
|
3
|
+
* allowlist). */
|
|
4
|
+
export declare function quoteIdent(id: string): string;
|
|
5
|
+
/** `"schema"."name"` for a resolved resource. */
|
|
6
|
+
export declare function qualified(resource: Resource): string;
|
|
7
|
+
//# sourceMappingURL=ident.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ident.d.ts","sourceRoot":"","sources":["../src/ident.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD;kBACkB;AAClB,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,iDAAiD;AACjD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAEpD"}
|
package/dist/ident.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Identifier quoting + qualified table names. Shared by the query builder
|
|
2
|
+
// and the embed fragment renderer so both quote identifiers identically.
|
|
3
|
+
/** Quote an identifier for safe inlining (defense in depth on top of the
|
|
4
|
+
* allowlist). */
|
|
5
|
+
export function quoteIdent(id) {
|
|
6
|
+
return '"' + id.replace(/"/g, '""') + '"';
|
|
7
|
+
}
|
|
8
|
+
/** `"schema"."name"` for a resolved resource. */
|
|
9
|
+
export function qualified(resource) {
|
|
10
|
+
return `${quoteIdent(resource.schema)}.${quoteIdent(resource.name)}`;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=ident.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ident.js","sourceRoot":"","sources":["../src/ident.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,yEAAyE;AAIzE;kBACkB;AAClB,MAAM,UAAU,UAAU,CAAC,EAAU;IACnC,OAAO,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,GAAG,CAAC;AAC5C,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,SAAS,CAAC,QAAkB;IAC1C,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;AACvE,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { startApiServer, createApiRequestListener, isLoopbackHost, type StartApiServerOptions, type ApiServerHandle, type PoolClient, type ConnectionPool, } from './startApiServer.js';
|
|
2
|
+
export { createAuthenticator, signServiceToken, type AuthConfig, type AuthContext, type Authenticator, type JwtAlgorithm, type ServiceTokenOptions, } from './auth.js';
|
|
3
|
+
export { handleApiRequest, parseListParams, type Queryable, type ApiHandlerDeps, type ApiHttpRequest, type ApiHttpResult, } from './handler.js';
|
|
4
|
+
export { buildResourceLookup, type Resource, type ResourceKind, type ResourceLookup, type ReverseRelation, } from './schema-lookup.js';
|
|
5
|
+
export { buildListQuery, buildGetQuery, buildInsertQuery, buildUpdateQuery, buildDeleteQuery, buildRelationOptionsQuery, quoteIdent, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, DEFAULT_RELATION_LIMIT, MAX_RELATION_LIMIT, type ListQueryParams, type BuiltListQuery, type BuiltGetQuery, type BuiltMutation, type RelationOptionsParams, type BuiltRelationOptions, type SortDirection, } from './query-builder.js';
|
|
6
|
+
export { buildOpenApiDocument, type OpenApiOptions } from './openapi.js';
|
|
7
|
+
export { parseEmbedParam, resolveEmbedSpec, buildEmbedSelectFragment, MAX_EMBED_DEPTH, MAX_EMBED_RELATIONS, MAX_EMBED_CHILDREN, type EmbedKind, type EmbedNode, type EmbedSpec, } from './embed.js';
|
|
8
|
+
export { KozouApiError, type ApiErrorBody } from './errors.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,cAAc,EACd,wBAAwB,EACxB,cAAc,EACd,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,KAAK,UAAU,EACf,KAAK,cAAc,GACpB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,mBAAmB,GACzB,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,mBAAmB,EACnB,KAAK,QAAQ,EACb,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,yBAAyB,EACzB,UAAU,EACV,iBAAiB,EACjB,aAAa,EACb,sBAAsB,EACtB,kBAAkB,EAClB,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,aAAa,GACnB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAEzE,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,wBAAwB,EACxB,eAAe,EACf,mBAAmB,EACnB,kBAAkB,EAClB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,SAAS,GACf,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,aAAa,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @kozou/api: Kozou's own REST layer (experimental, Kozou v0.2).
|
|
2
|
+
// Serves the tables and views of a SchemaContext as a REST API. See the
|
|
3
|
+
// Kozou v0.2 design spec §2–§4.
|
|
4
|
+
export { startApiServer, createApiRequestListener, isLoopbackHost, } from './startApiServer.js';
|
|
5
|
+
export { createAuthenticator, signServiceToken, } from './auth.js';
|
|
6
|
+
export { handleApiRequest, parseListParams, } from './handler.js';
|
|
7
|
+
export { buildResourceLookup, } from './schema-lookup.js';
|
|
8
|
+
export { buildListQuery, buildGetQuery, buildInsertQuery, buildUpdateQuery, buildDeleteQuery, buildRelationOptionsQuery, quoteIdent, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, DEFAULT_RELATION_LIMIT, MAX_RELATION_LIMIT, } from './query-builder.js';
|
|
9
|
+
export { buildOpenApiDocument } from './openapi.js';
|
|
10
|
+
export { parseEmbedParam, resolveEmbedSpec, buildEmbedSelectFragment, MAX_EMBED_DEPTH, MAX_EMBED_RELATIONS, MAX_EMBED_CHILDREN, } from './embed.js';
|
|
11
|
+
export { KozouApiError } from './errors.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,wEAAwE;AACxE,gCAAgC;AAEhC,OAAO,EACL,cAAc,EACd,wBAAwB,EACxB,cAAc,GAKf,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,mBAAmB,EACnB,gBAAgB,GAMjB,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,gBAAgB,EAChB,eAAe,GAKhB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,mBAAmB,GAKpB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,yBAAyB,EACzB,UAAU,EACV,iBAAiB,EACjB,aAAa,EACb,sBAAsB,EACtB,kBAAkB,GAQnB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,oBAAoB,EAAuB,MAAM,cAAc,CAAC;AAEzE,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,wBAAwB,EACxB,eAAe,EACf,mBAAmB,EACnB,kBAAkB,GAInB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,aAAa,EAAqB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SchemaContext } from '@kozou/core';
|
|
2
|
+
export type OpenApiOptions = {
|
|
3
|
+
title?: string;
|
|
4
|
+
version?: string;
|
|
5
|
+
};
|
|
6
|
+
type JsonObject = Record<string, unknown>;
|
|
7
|
+
export declare function buildOpenApiDocument(schema: SchemaContext, opts?: OpenApiOptions): JsonObject;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=openapi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EACV,aAAa,EAMd,MAAM,aAAa,CAAC;AAErB,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,KAAK,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAK1C,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,aAAa,EACrB,IAAI,GAAE,cAAmB,GACxB,UAAU,CAmDZ"}
|