@kozou/api 0.0.1 → 1.0.0

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.
@@ -0,0 +1,46 @@
1
+ import type { RelationContext } from '@kozou/core';
2
+ import type { Resource, ResourceLookup } from './schema-lookup.js';
3
+ /** Maximum relation chain length (dot-separated segments) per embed path. */
4
+ export declare const MAX_EMBED_DEPTH = 5;
5
+ /** Maximum number of distinct relations a single request may embed. */
6
+ export declare const MAX_EMBED_RELATIONS = 25;
7
+ /** Maximum number of child rows inlined per parent for a to-many embed. */
8
+ export declare const MAX_EMBED_CHILDREN = 100;
9
+ export type EmbedKind = 'to-one' | 'to-many';
10
+ export type EmbedNode = {
11
+ /** `to-one`: a forward FK on the parent. `to-many`: a child's FK pointing
12
+ * back at the parent. */
13
+ kind: EmbedKind;
14
+ /** The foreign key linking parent and target. For `to-one` it lives on the
15
+ * parent (`relation.field`) and points at the target (`references.column`);
16
+ * for `to-many` it lives on the child target (`relation.field`) and points
17
+ * back at the parent (`references.column`). */
18
+ relation: RelationContext;
19
+ /** The resolved target resource; its columns form the nested allowlist. */
20
+ target: Resource;
21
+ /** The key the nested value is returned under. */
22
+ key: string;
23
+ /** Deeper embeds, resolved against `target`. */
24
+ children: EmbedNode[];
25
+ };
26
+ export type EmbedSpec = EmbedNode[];
27
+ /** Split a raw `embed` value into paths. Pure: no schema knowledge.
28
+ * `"author,editions.books"` -> `[["author"], ["editions", "books"]]`. */
29
+ export declare function parseEmbedParam(raw: string | null | undefined): string[][];
30
+ /** Resolve + validate parsed paths into an embed forest. Throws `badRequest`
31
+ * on any unknown / ambiguous selector, over-cap depth or count, or
32
+ * unembeddable target. Paths sharing a prefix are merged so a relation is
33
+ * embedded at most once per parent. */
34
+ export declare function resolveEmbedSpec(root: Resource, paths: string[][], lookup: ResourceLookup): EmbedSpec;
35
+ /** Render the SELECT-list fragment for an embed forest, composed recursively
36
+ * for depth. `parentRef` is the SQL reference for the parent row scope (the
37
+ * qualified table name at the top level, a generated alias below); `counter`
38
+ * keeps aliases unique across the whole statement. Identifiers only — no
39
+ * bound parameters are produced.
40
+ *
41
+ * - to-one -> `(SELECT to_jsonb(eN) FROM (...) eN) AS "key"` (object | null)
42
+ * - to-many -> `(SELECT coalesce(jsonb_agg(...), '[]') FROM (...) eN) AS "key"` (array) */
43
+ export declare function buildEmbedSelectFragment(spec: EmbedNode[], parentRef: string, counter: {
44
+ n: number;
45
+ }): string;
46
+ //# sourceMappingURL=embed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAmB,MAAM,oBAAoB,CAAC;AAEpF,6EAA6E;AAC7E,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE7C,MAAM,MAAM,SAAS,GAAG;IACtB;8BAC0B;IAC1B,IAAI,EAAE,SAAS,CAAC;IAChB;;;oDAGgD;IAChD,QAAQ,EAAE,eAAe,CAAC;IAC1B,2EAA2E;IAC3E,MAAM,EAAE,QAAQ,CAAC;IACjB,kDAAkD;IAClD,GAAG,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,QAAQ,EAAE,SAAS,EAAE,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,SAAS,EAAE,CAAC;AAEpC;0EAC0E;AAC1E,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,EAAE,CAW1E;AAED;;;wCAGwC;AACxC,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,MAAM,EAAE,EAAE,EACjB,MAAM,EAAE,cAAc,GACrB,SAAS,CAUX;AAsHD;;;;;;;4FAO4F;AAC5F,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,SAAS,EAAE,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GACrB,MAAM,CAuBR"}
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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"}
@@ -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,39 @@
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
+ /** List query keys consumed as controls (not column filters). Shared with the
34
+ * OpenAPI generator so it never advertises a control key as a filterable
35
+ * column. */
36
+ export declare const RESERVED_PARAMS: Set<string>;
37
+ export declare function handleApiRequest(deps: ApiHandlerDeps, req: ApiHttpRequest): Promise<ApiHttpResult>;
38
+ export declare function parseListParams(query: URLSearchParams): ListQueryParams;
39
+ //# 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,EAIrB,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;AAEF;;cAEc;AACd,eAAO,MAAM,eAAe,aAA2D,CAAC;AAExF,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,CA0BvE"}
@@ -0,0 +1,267 @@
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
+ /** List query keys consumed as controls (not column filters). Shared with the
8
+ * OpenAPI generator so it never advertises a control key as a filterable
9
+ * column. */
10
+ export const RESERVED_PARAMS = new Set(['page', 'pageSize', 'sort', 'search', 'embed']);
11
+ export async function handleApiRequest(deps, req) {
12
+ try {
13
+ return await route(deps, req);
14
+ }
15
+ catch (err) {
16
+ if (err instanceof KozouApiError) {
17
+ return { status: err.status, body: errorBody(err.code, err.message) };
18
+ }
19
+ const message = err instanceof Error ? err.message : String(err);
20
+ return { status: 500, body: errorBody('internal', message) };
21
+ }
22
+ }
23
+ async function route(deps, req) {
24
+ const { method, segments, query } = req;
25
+ const m = method.toUpperCase();
26
+ if (segments.length === 0) {
27
+ requireMethod(method, 'GET');
28
+ return {
29
+ status: 200,
30
+ body: { name: 'kozou-api', version: deps.version ?? null, resources: deps.lookup.list() },
31
+ };
32
+ }
33
+ if (segments.length === 1 && segments[0] === 'openapi.json') {
34
+ requireMethod(method, 'GET');
35
+ if (!deps.openapi) {
36
+ throw notFound('OpenAPI document is not configured.');
37
+ }
38
+ return { status: 200, body: deps.openapi };
39
+ }
40
+ if (segments.length === 1) {
41
+ const resource = resolveOr404(deps.lookup, segments[0]);
42
+ if (m === 'GET') {
43
+ return query.get('as') === 'options'
44
+ ? relationOptions(deps, resource, query)
45
+ : listResource(deps, resource, query);
46
+ }
47
+ if (m === 'POST')
48
+ return createResource(deps, resource, req.body);
49
+ throw methodNotAllowed(`Method ${method} not allowed on a collection; use GET or POST.`);
50
+ }
51
+ if (segments.length === 2) {
52
+ const resource = resolveOr404(deps.lookup, segments[0]);
53
+ const id = segments[1];
54
+ if (m === 'GET')
55
+ return getResource(deps, resource, id, query);
56
+ if (m === 'PATCH')
57
+ return updateResource(deps, resource, id, req.body);
58
+ if (m === 'DELETE')
59
+ return deleteResource(deps, resource, id);
60
+ throw methodNotAllowed(`Method ${method} not allowed on an item; use GET, PATCH, or DELETE.`);
61
+ }
62
+ throw notFound(`No route for /${segments.join('/')}.`);
63
+ }
64
+ async function listResource(deps, resource, query) {
65
+ const params = parseListParams(query);
66
+ const embed = resolveEmbedSpec(resource, parseEmbedParam(query.get('embed')), deps.lookup);
67
+ const built = buildListQuery(resource, { ...params, embed });
68
+ const [dataResult, countResult] = await Promise.all([
69
+ deps.db.query(built.dataText, built.dataValues),
70
+ deps.db.query(built.countText, built.countValues),
71
+ ]);
72
+ const total = Number(countResult.rows[0]?.total ?? 0);
73
+ return {
74
+ status: 200,
75
+ body: { rows: dataResult.rows, total, page: built.page, pageSize: built.pageSize },
76
+ };
77
+ }
78
+ async function getResource(deps, resource, id, query) {
79
+ const embed = resolveEmbedSpec(resource, parseEmbedParam(query.get('embed')), deps.lookup);
80
+ const built = buildGetQuery(resource, id, embed);
81
+ const result = await deps.db.query(built.text, built.values);
82
+ if (result.rows.length === 0) {
83
+ return notFoundResult(resource, id);
84
+ }
85
+ return { status: 200, body: result.rows[0] };
86
+ }
87
+ async function createResource(deps, resource, body) {
88
+ requireWritable(resource);
89
+ const built = buildInsertQuery(resource, requireObjectBody(body));
90
+ const result = await deps.db.query(built.text, built.values);
91
+ return { status: 201, body: result.rows[0] };
92
+ }
93
+ async function updateResource(deps, resource, id, body) {
94
+ requireWritable(resource);
95
+ const built = buildUpdateQuery(resource, id, requireObjectBody(body));
96
+ const result = await deps.db.query(built.text, built.values);
97
+ if (result.rows.length === 0)
98
+ return notFoundResult(resource, id);
99
+ return { status: 200, body: result.rows[0] };
100
+ }
101
+ async function deleteResource(deps, resource, id) {
102
+ requireWritable(resource);
103
+ const built = buildDeleteQuery(resource, id);
104
+ const result = await deps.db.query(built.text, built.values);
105
+ if (result.rows.length === 0)
106
+ return notFoundResult(resource, id);
107
+ return { status: 200, body: result.rows[0] };
108
+ }
109
+ async function relationOptions(deps, resource, query) {
110
+ const labelField = query.get('label');
111
+ if (labelField === null || labelField.length === 0) {
112
+ throw badRequest('Relation options require a "label" query parameter.');
113
+ }
114
+ const fieldsRaw = query.get('fields');
115
+ const searchFields = fieldsRaw
116
+ ? fieldsRaw.split(',').map((s) => s.trim()).filter((s) => s.length > 0)
117
+ : [];
118
+ const built = buildRelationOptionsQuery(resource, {
119
+ labelField,
120
+ searchFields,
121
+ query: query.get('q') ?? undefined,
122
+ limit: parsePositiveInt(query.get('limit')),
123
+ });
124
+ const result = await deps.db.query(built.text, built.values);
125
+ const options = result.rows.map((row) => ({
126
+ id: row[built.primaryKey],
127
+ label: String(row[built.labelField] ?? ''),
128
+ }));
129
+ return { status: 200, body: { options } };
130
+ }
131
+ function notFoundResult(resource, id) {
132
+ return {
133
+ status: 404,
134
+ body: errorBody('not_found', `No "${resource.name}" row with id "${id}".`),
135
+ };
136
+ }
137
+ function requireWritable(resource) {
138
+ if (resource.kind === 'view') {
139
+ throw methodNotAllowed(`Resource "${resource.name}" is a read-only view.`);
140
+ }
141
+ }
142
+ function requireObjectBody(body) {
143
+ if (body === null || typeof body !== 'object' || Array.isArray(body)) {
144
+ throw badRequest('Request body must be a JSON object.');
145
+ }
146
+ return body;
147
+ }
148
+ function resolveOr404(lookup, name) {
149
+ const resource = lookup.resolve(name);
150
+ if (!resource) {
151
+ throw notFound(`Unknown resource "${name}".`);
152
+ }
153
+ return resource;
154
+ }
155
+ function requireMethod(method, allowed) {
156
+ if (method.toUpperCase() !== allowed) {
157
+ throw methodNotAllowed(`Method ${method} not allowed here; use ${allowed}.`);
158
+ }
159
+ }
160
+ export function parseListParams(query) {
161
+ const params = {};
162
+ const page = parsePositiveInt(query.get('page'));
163
+ if (page !== undefined)
164
+ params.page = page;
165
+ const pageSize = parsePositiveInt(query.get('pageSize'));
166
+ if (pageSize !== undefined)
167
+ params.pageSize = pageSize;
168
+ const search = query.get('search');
169
+ if (search !== null && search.length > 0)
170
+ params.search = search;
171
+ const sort = parseSort(query.get('sort'));
172
+ if (sort.length > 0)
173
+ params.sort = sort;
174
+ // Each non-reserved query entry is one horizontal filter. Repeated keys are
175
+ // kept (not collapsed) so a column can carry several filters — e.g. a
176
+ // `?price=gte.10&price=lte.20` range — that combine with AND.
177
+ const filters = [];
178
+ for (const [key, value] of query.entries()) {
179
+ if (RESERVED_PARAMS.has(key))
180
+ continue;
181
+ filters.push(parseFilter(key, value));
182
+ }
183
+ if (filters.length > 0)
184
+ params.filters = filters;
185
+ return params;
186
+ }
187
+ const FILTER_OPERATORS = new Set([
188
+ 'eq',
189
+ 'neq',
190
+ 'gt',
191
+ 'gte',
192
+ 'lt',
193
+ 'lte',
194
+ 'like',
195
+ 'ilike',
196
+ 'in',
197
+ 'is',
198
+ ]);
199
+ function isFilterOperator(token) {
200
+ return FILTER_OPERATORS.has(token);
201
+ }
202
+ /**
203
+ * Parse one `?<column>=<raw>` entry into a structured {@link Filter}.
204
+ *
205
+ * Grammar: `<op>.<value>` where `op` is one of the known operators; the split
206
+ * is on the FIRST `.` only (so values may contain dots). A value whose prefix
207
+ * is not a known operator — or which has no leading `op.` at all — is treated
208
+ * as an equality match, keeping the legacy `?<column>=<value>` form working.
209
+ * The split uses `indexOf`, not a regular expression (ReDoS-safe).
210
+ */
211
+ function parseFilter(column, raw) {
212
+ const dot = raw.indexOf('.');
213
+ if (dot > 0) {
214
+ const maybeOp = raw.slice(0, dot);
215
+ if (isFilterOperator(maybeOp)) {
216
+ return buildFilter(column, maybeOp, raw.slice(dot + 1));
217
+ }
218
+ }
219
+ return { column, op: 'eq', value: raw };
220
+ }
221
+ function buildFilter(column, op, rhs) {
222
+ if (op === 'in') {
223
+ if (rhs.length < 2 || rhs[0] !== '(' || rhs[rhs.length - 1] !== ')') {
224
+ throw badRequest(`Filter "${column}=in.${rhs}" must look like "in.(v1,v2,...)".`);
225
+ }
226
+ const inner = rhs.slice(1, -1);
227
+ // Note: values cannot contain a comma (no quoting is supported in v1.0).
228
+ const values = inner.length === 0 ? [] : inner.split(',');
229
+ return { column, op: 'in', values };
230
+ }
231
+ if (op === 'is') {
232
+ if (rhs === 'null' || rhs === 'notnull' || rhs === 'true' || rhs === 'false') {
233
+ return { column, op: 'is', keyword: rhs };
234
+ }
235
+ throw badRequest(`Filter "${column}=is.${rhs}" must be one of is.null, is.notnull, is.true, is.false.`);
236
+ }
237
+ return { column, op, value: rhs };
238
+ }
239
+ function parsePositiveInt(raw) {
240
+ if (raw === null || raw.length === 0)
241
+ return undefined;
242
+ const n = Number(raw);
243
+ if (!Number.isFinite(n))
244
+ return undefined;
245
+ return n;
246
+ }
247
+ function parseSort(raw) {
248
+ if (raw === null || raw.length === 0)
249
+ return [];
250
+ const result = [];
251
+ for (const token of raw.split(',')) {
252
+ const trimmed = token.trim();
253
+ if (trimmed.length === 0)
254
+ continue;
255
+ const dot = trimmed.lastIndexOf('.');
256
+ if (dot > 0) {
257
+ const suffix = trimmed.slice(dot + 1).toLowerCase();
258
+ if (suffix === 'asc' || suffix === 'desc') {
259
+ result.push({ field: trimmed.slice(0, dot), order: suffix });
260
+ continue;
261
+ }
262
+ }
263
+ result.push({ field: trimmed, order: 'asc' });
264
+ }
265
+ return result;
266
+ }
267
+ //# sourceMappingURL=handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler.js","sourceRoot":"","sources":["../src/handler.ts"],"names":[],"mappings":"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"}
@@ -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