@veducio/keja 0.3.0 → 0.5.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.
- package/package.json +1 -1
- package/src/grant/grant.test.ts +116 -57
- package/src/grant/grant.ts +47 -26
- package/src/grant/settings.ts +67 -0
- package/src/http/batch.ts +307 -0
- package/src/http/create-app.test.ts +141 -0
- package/src/http/create-app.ts +25 -0
- package/src/http/openapi.ts +94 -0
- package/src/project/config.test.ts +39 -3
- package/src/project/config.ts +163 -16
- package/src/project/load.ts +6 -0
- package/src/repository/generate.test.ts +16 -0
- package/src/repository/generate.ts +11 -4
- package/src/repository/query.ts +5 -0
- package/src/repository/runtime.test.ts +18 -0
- package/src/repository/runtime.ts +57 -34
- package/src/repository/sql.test.ts +33 -0
- package/src/repository/sql.ts +199 -27
- package/src/route/define.ts +14 -0
- package/src/server/index.ts +19 -1
- package/src/vault/client.test.ts +0 -6
|
@@ -100,12 +100,46 @@ describe("defineConfig", () => {
|
|
|
100
100
|
token: "s.token",
|
|
101
101
|
namespace: "apps",
|
|
102
102
|
mount: "kv",
|
|
103
|
-
transit_key: null,
|
|
104
|
-
transit_mount: "transit",
|
|
105
|
-
grant_max_exp: 900,
|
|
106
103
|
});
|
|
107
104
|
expect(defineConfig().vault).toEqual({ enabled: false });
|
|
108
105
|
});
|
|
106
|
+
|
|
107
|
+
it("reads grants and batch settings from config", () => {
|
|
108
|
+
expect(
|
|
109
|
+
defineConfig({
|
|
110
|
+
grants: {
|
|
111
|
+
enabled: true,
|
|
112
|
+
mode: "transit",
|
|
113
|
+
transit_key: "keja-grants",
|
|
114
|
+
},
|
|
115
|
+
}).grants,
|
|
116
|
+
).toEqual({
|
|
117
|
+
enabled: true,
|
|
118
|
+
mode: "transit",
|
|
119
|
+
max_exp: 900,
|
|
120
|
+
transit_key: "keja-grants",
|
|
121
|
+
transit_mount: "transit",
|
|
122
|
+
});
|
|
123
|
+
expect(
|
|
124
|
+
defineConfig({
|
|
125
|
+
grants: {
|
|
126
|
+
enabled: true,
|
|
127
|
+
mode: "secret",
|
|
128
|
+
secret: "test-secret",
|
|
129
|
+
},
|
|
130
|
+
}).grants,
|
|
131
|
+
).toEqual({
|
|
132
|
+
enabled: true,
|
|
133
|
+
mode: "secret",
|
|
134
|
+
max_exp: 900,
|
|
135
|
+
secret: "test-secret",
|
|
136
|
+
secret_vault: null,
|
|
137
|
+
secret_field: "secret",
|
|
138
|
+
});
|
|
139
|
+
expect(defineConfig({ batch: false }).batch).toEqual({ enabled: false });
|
|
140
|
+
expect(defineConfig().grants).toEqual({ enabled: false });
|
|
141
|
+
expect(defineConfig().batch).toEqual({ enabled: true });
|
|
142
|
+
});
|
|
109
143
|
});
|
|
110
144
|
|
|
111
145
|
describe("loadConfig", () => {
|
|
@@ -117,6 +151,8 @@ describe("loadConfig", () => {
|
|
|
117
151
|
argus: { enabled: false },
|
|
118
152
|
database: { enabled: false },
|
|
119
153
|
vault: { enabled: false },
|
|
154
|
+
grants: { enabled: false },
|
|
155
|
+
batch: { enabled: true },
|
|
120
156
|
versioning: path.resolve(root, "keja.versioning.ts"),
|
|
121
157
|
public: path.resolve(root, "src/public"),
|
|
122
158
|
service: path.resolve(root, "src/service"),
|
package/src/project/config.ts
CHANGED
|
@@ -49,9 +49,6 @@ export type VaultSettings = {
|
|
|
49
49
|
token: string;
|
|
50
50
|
namespace: string | null;
|
|
51
51
|
mount: string;
|
|
52
|
-
transit_key: string | null;
|
|
53
|
-
transit_mount: string;
|
|
54
|
-
grant_max_exp: number;
|
|
55
52
|
};
|
|
56
53
|
|
|
57
54
|
export type VaultConfigInput = {
|
|
@@ -60,15 +57,45 @@ export type VaultConfigInput = {
|
|
|
60
57
|
token?: string;
|
|
61
58
|
namespace?: string;
|
|
62
59
|
mount?: string;
|
|
63
|
-
transit_key?: string;
|
|
64
|
-
transit_mount?: string;
|
|
65
|
-
grant_max_exp?: number;
|
|
66
60
|
};
|
|
67
61
|
|
|
68
62
|
export type VaultConfig =
|
|
69
63
|
| { enabled: false }
|
|
70
64
|
| ({ enabled: true } & VaultSettings);
|
|
71
65
|
|
|
66
|
+
export type GrantsConfigInput = {
|
|
67
|
+
enabled: boolean;
|
|
68
|
+
mode?: "transit" | "secret";
|
|
69
|
+
max_exp?: number;
|
|
70
|
+
transit_key?: string;
|
|
71
|
+
transit_mount?: string;
|
|
72
|
+
secret?: string;
|
|
73
|
+
secret_vault?: string;
|
|
74
|
+
secret_field?: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type GrantsConfig =
|
|
78
|
+
| { enabled: false }
|
|
79
|
+
| {
|
|
80
|
+
enabled: true;
|
|
81
|
+
mode: "transit";
|
|
82
|
+
max_exp: number;
|
|
83
|
+
transit_key: string;
|
|
84
|
+
transit_mount: string;
|
|
85
|
+
}
|
|
86
|
+
| {
|
|
87
|
+
enabled: true;
|
|
88
|
+
mode: "secret";
|
|
89
|
+
max_exp: number;
|
|
90
|
+
secret: string | null;
|
|
91
|
+
secret_vault: string | null;
|
|
92
|
+
secret_field: string;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type BatchConfigInput = boolean | { enabled: boolean };
|
|
96
|
+
|
|
97
|
+
export type BatchConfig = { enabled: boolean };
|
|
98
|
+
|
|
72
99
|
export type KejaConfigInput = {
|
|
73
100
|
name?: string;
|
|
74
101
|
versioning?: string;
|
|
@@ -77,6 +104,8 @@ export type KejaConfigInput = {
|
|
|
77
104
|
argus?: ArgusConfigInput;
|
|
78
105
|
db?: DatabaseConfigInput;
|
|
79
106
|
vault?: VaultConfigInput;
|
|
107
|
+
grants?: GrantsConfigInput;
|
|
108
|
+
batch?: BatchConfigInput;
|
|
80
109
|
};
|
|
81
110
|
|
|
82
111
|
export type KejaConfig = {
|
|
@@ -87,6 +116,8 @@ export type KejaConfig = {
|
|
|
87
116
|
argus: ArgusConfig;
|
|
88
117
|
database: DatabaseConfig;
|
|
89
118
|
vault: VaultConfig;
|
|
119
|
+
grants: GrantsConfig;
|
|
120
|
+
batch: BatchConfig;
|
|
90
121
|
};
|
|
91
122
|
|
|
92
123
|
export function defineConfig(input: KejaConfigInput = {}) {
|
|
@@ -110,6 +141,8 @@ export function defineConfig(input: KejaConfigInput = {}) {
|
|
|
110
141
|
argus: configArgus(input.argus),
|
|
111
142
|
database: configDatabase(input.db),
|
|
112
143
|
vault: configVault(input.vault),
|
|
144
|
+
grants: configGrants(input.grants),
|
|
145
|
+
batch: configBatch(input.batch),
|
|
113
146
|
};
|
|
114
147
|
return Object.assign(config, { [configBrand]: true as const });
|
|
115
148
|
}
|
|
@@ -126,7 +159,9 @@ export function isConfig(value: unknown): value is KejaConfig {
|
|
|
126
159
|
hasName(value) &&
|
|
127
160
|
hasArgus(value) &&
|
|
128
161
|
hasDatabase(value) &&
|
|
129
|
-
hasVault(value)
|
|
162
|
+
hasVault(value) &&
|
|
163
|
+
hasGrants(value) &&
|
|
164
|
+
hasBatch(value)
|
|
130
165
|
);
|
|
131
166
|
}
|
|
132
167
|
|
|
@@ -142,6 +177,8 @@ export type LoadedConfig = ProjectPaths & {
|
|
|
142
177
|
argus: ArgusConfig;
|
|
143
178
|
database: DatabaseConfig;
|
|
144
179
|
vault: VaultConfig;
|
|
180
|
+
grants: GrantsConfig;
|
|
181
|
+
batch: BatchConfig;
|
|
145
182
|
};
|
|
146
183
|
|
|
147
184
|
export function resolveProjectPaths(root: string, config: KejaConfig) {
|
|
@@ -198,6 +235,8 @@ function loadedConfig(
|
|
|
198
235
|
argus: config.argus,
|
|
199
236
|
database: resolvedDatabase(root, config.database),
|
|
200
237
|
vault: config.vault,
|
|
238
|
+
grants: config.grants,
|
|
239
|
+
batch: config.batch,
|
|
201
240
|
};
|
|
202
241
|
}
|
|
203
242
|
|
|
@@ -295,18 +334,90 @@ function configVault(value: VaultConfigInput | undefined): VaultConfig {
|
|
|
295
334
|
),
|
|
296
335
|
namespace: optionalText(namespace, "vault.namespace", "namespace"),
|
|
297
336
|
mount: vaultMount(value.mount ?? configEnv("VAULT_KV_MOUNT") ?? "secret"),
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function configGrants(value: GrantsConfigInput | undefined): GrantsConfig {
|
|
341
|
+
if (value === undefined) {
|
|
342
|
+
return { enabled: false };
|
|
343
|
+
}
|
|
344
|
+
if (!isEnabledObject(value)) {
|
|
345
|
+
throw projectError(
|
|
346
|
+
"invalidConfigFile",
|
|
347
|
+
"keja.config.ts grants must be an object with enabled",
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if (!value.enabled) {
|
|
351
|
+
return { enabled: false };
|
|
352
|
+
}
|
|
353
|
+
const mode = value.mode ?? "transit";
|
|
354
|
+
if (mode !== "transit" && mode !== "secret") {
|
|
355
|
+
throw projectError(
|
|
356
|
+
"invalidConfigFile",
|
|
357
|
+
"keja.config.ts grants.mode must be transit or secret",
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const max_exp = grantMaxExp(value.max_exp);
|
|
361
|
+
if (mode === "transit") {
|
|
362
|
+
return {
|
|
363
|
+
enabled: true,
|
|
364
|
+
mode: "transit",
|
|
365
|
+
max_exp,
|
|
366
|
+
transit_key: vaultName(
|
|
367
|
+
requiredText(value.transit_key, "grants.transit_key", "key name"),
|
|
368
|
+
"grants.transit_key",
|
|
369
|
+
),
|
|
370
|
+
transit_mount: vaultName(
|
|
371
|
+
value.transit_mount ?? "transit",
|
|
372
|
+
"grants.transit_mount",
|
|
373
|
+
),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const secret =
|
|
377
|
+
value.secret === undefined
|
|
378
|
+
? (configEnv("GRANT_SECRET") ?? null)
|
|
379
|
+
: requiredText(value.secret, "grants.secret", "secret");
|
|
380
|
+
const secret_vault =
|
|
381
|
+
value.secret_vault === undefined
|
|
382
|
+
? null
|
|
383
|
+
: secretVaultPath(
|
|
384
|
+
requiredText(value.secret_vault, "grants.secret_vault", "path"),
|
|
385
|
+
);
|
|
386
|
+
if (secret === null && secret_vault === null) {
|
|
387
|
+
throw projectError(
|
|
388
|
+
"invalidConfigFile",
|
|
389
|
+
"keja.config.ts grants.secret, GRANT_SECRET, or grants.secret_vault is required",
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
enabled: true,
|
|
394
|
+
mode: "secret",
|
|
395
|
+
max_exp,
|
|
396
|
+
secret,
|
|
397
|
+
secret_vault,
|
|
398
|
+
secret_field: vaultName(
|
|
399
|
+
value.secret_field ?? "secret",
|
|
400
|
+
"grants.secret_field",
|
|
305
401
|
),
|
|
306
|
-
grant_max_exp: grantMaxExp(value.grant_max_exp),
|
|
307
402
|
};
|
|
308
403
|
}
|
|
309
404
|
|
|
405
|
+
function configBatch(value: BatchConfigInput | undefined): BatchConfig {
|
|
406
|
+
if (value === undefined) {
|
|
407
|
+
return { enabled: true };
|
|
408
|
+
}
|
|
409
|
+
if (typeof value === "boolean") {
|
|
410
|
+
return { enabled: value };
|
|
411
|
+
}
|
|
412
|
+
if (!isEnabledObject(value)) {
|
|
413
|
+
throw projectError(
|
|
414
|
+
"invalidConfigFile",
|
|
415
|
+
"keja.config.ts batch must be a boolean or an object with enabled",
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
return { enabled: value.enabled };
|
|
419
|
+
}
|
|
420
|
+
|
|
310
421
|
function isEnabledObject(value: unknown): value is { enabled: boolean } {
|
|
311
422
|
return (
|
|
312
423
|
typeof value === "object" &&
|
|
@@ -354,6 +465,21 @@ function vaultName(value: string, field: string) {
|
|
|
354
465
|
return name;
|
|
355
466
|
}
|
|
356
467
|
|
|
468
|
+
function secretVaultPath(value: string) {
|
|
469
|
+
const cleaned = value.trim().replace(/^\/+|\/+$/g, "");
|
|
470
|
+
const parts = cleaned.split("/");
|
|
471
|
+
if (
|
|
472
|
+
!cleaned ||
|
|
473
|
+
parts.some((part) => part.length === 0 || part === "." || part === "..")
|
|
474
|
+
) {
|
|
475
|
+
throw projectError(
|
|
476
|
+
"invalidConfigFile",
|
|
477
|
+
"keja.config.ts grants.secret_vault must be a Vault secret path",
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
return cleaned;
|
|
481
|
+
}
|
|
482
|
+
|
|
357
483
|
function grantMaxExp(value: number | undefined) {
|
|
358
484
|
if (value === undefined) {
|
|
359
485
|
return 900;
|
|
@@ -366,7 +492,7 @@ function grantMaxExp(value: number | undefined) {
|
|
|
366
492
|
) {
|
|
367
493
|
throw projectError(
|
|
368
494
|
"invalidConfigFile",
|
|
369
|
-
"keja.config.ts
|
|
495
|
+
"keja.config.ts grants.max_exp must be a whole number of seconds from 1 to 86400",
|
|
370
496
|
);
|
|
371
497
|
}
|
|
372
498
|
return value;
|
|
@@ -435,6 +561,27 @@ function hasVault(value: object) {
|
|
|
435
561
|
);
|
|
436
562
|
}
|
|
437
563
|
|
|
564
|
+
function hasGrants(value: object) {
|
|
565
|
+
const grants = (value as KejaConfig).grants;
|
|
566
|
+
return (
|
|
567
|
+
typeof grants === "object" &&
|
|
568
|
+
grants !== null &&
|
|
569
|
+
(grants.enabled === false ||
|
|
570
|
+
(grants.enabled === true &&
|
|
571
|
+
(grants.mode === "transit" || grants.mode === "secret") &&
|
|
572
|
+
typeof grants.max_exp === "number"))
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function hasBatch(value: object) {
|
|
577
|
+
const batch = (value as KejaConfig).batch;
|
|
578
|
+
return (
|
|
579
|
+
typeof batch === "object" &&
|
|
580
|
+
batch !== null &&
|
|
581
|
+
typeof batch.enabled === "boolean"
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
438
585
|
function hasDatabase(value: object) {
|
|
439
586
|
const database = (value as KejaConfig).database;
|
|
440
587
|
return (
|
package/src/project/load.ts
CHANGED
|
@@ -11,7 +11,9 @@ import {
|
|
|
11
11
|
import { isVersions, type VersionDefinition } from "../versioning/define.ts";
|
|
12
12
|
import {
|
|
13
13
|
type ArgusConfig,
|
|
14
|
+
type BatchConfig,
|
|
14
15
|
type DatabaseConfig,
|
|
16
|
+
type GrantsConfig,
|
|
15
17
|
loadConfig,
|
|
16
18
|
type ProjectPaths,
|
|
17
19
|
type VaultConfig,
|
|
@@ -23,6 +25,8 @@ export type LoadedProject = {
|
|
|
23
25
|
name: string | null;
|
|
24
26
|
argus: ArgusConfig;
|
|
25
27
|
vault: VaultConfig;
|
|
28
|
+
grants: GrantsConfig;
|
|
29
|
+
batch: BatchConfig;
|
|
26
30
|
database: DatabaseConfig;
|
|
27
31
|
versions: VersionDefinition[];
|
|
28
32
|
publicRoutes: RouteDefinition[];
|
|
@@ -58,6 +62,8 @@ export async function loadProject(
|
|
|
58
62
|
name: loaded.name,
|
|
59
63
|
argus: loaded.argus,
|
|
60
64
|
vault: loaded.vault,
|
|
65
|
+
grants: loaded.grants,
|
|
66
|
+
batch: loaded.batch,
|
|
61
67
|
database: loaded.database,
|
|
62
68
|
versions: versioning.default.versions,
|
|
63
69
|
publicRoutes: await loadRoutes(publicDirectory, generation),
|
|
@@ -55,4 +55,20 @@ describe("renderRepositories", () => {
|
|
|
55
55
|
);
|
|
56
56
|
expect(source).toContain('list: { filters: ["name"], limit: 20 },');
|
|
57
57
|
});
|
|
58
|
+
|
|
59
|
+
it("writes enum columns with their values", () => {
|
|
60
|
+
const tables = readTables([
|
|
61
|
+
{
|
|
62
|
+
file: "0001_example.sql",
|
|
63
|
+
sql: `
|
|
64
|
+
create type post_status as enum ('draft', 'published');
|
|
65
|
+
create table posts (${owned}, status post_status not null);
|
|
66
|
+
`,
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
const source = renderRepositories(tables, new Map());
|
|
70
|
+
expect(source).toContain(
|
|
71
|
+
'status: { type: "enum", values: ["draft", "published"], nullable: false },',
|
|
72
|
+
);
|
|
73
|
+
});
|
|
58
74
|
});
|
|
@@ -82,10 +82,7 @@ function renderRepository(
|
|
|
82
82
|
flags: { list: ResolvedList; get: boolean } | undefined,
|
|
83
83
|
) {
|
|
84
84
|
const columns = table.columns
|
|
85
|
-
.map(
|
|
86
|
-
(column) =>
|
|
87
|
-
` ${column.name}: { type: "${column.type}", nullable: ${column.nullable} },`,
|
|
88
|
-
)
|
|
85
|
+
.map((column) => renderColumn(column))
|
|
89
86
|
.join("\n");
|
|
90
87
|
const options = [
|
|
91
88
|
renderList(flags?.list),
|
|
@@ -115,3 +112,13 @@ function renderList(list: ResolvedList | undefined) {
|
|
|
115
112
|
].filter((part) => part !== "");
|
|
116
113
|
return ` list: { ${parts.join(", ")} },`;
|
|
117
114
|
}
|
|
115
|
+
|
|
116
|
+
function renderColumn(column: TableSchema["columns"][number]) {
|
|
117
|
+
if (column.type === "enum") {
|
|
118
|
+
const values = column.values
|
|
119
|
+
.map((value) => JSON.stringify(value))
|
|
120
|
+
.join(", ");
|
|
121
|
+
return ` ${column.name}: { type: "enum", values: [${values}], nullable: ${column.nullable} },`;
|
|
122
|
+
}
|
|
123
|
+
return ` ${column.name}: { type: "${column.type}", nullable: ${column.nullable} },`;
|
|
124
|
+
}
|
package/src/repository/query.ts
CHANGED
|
@@ -245,6 +245,11 @@ function parseValue(column: DomainColumn, value: string) {
|
|
|
245
245
|
}
|
|
246
246
|
return value;
|
|
247
247
|
}
|
|
248
|
+
if (column.type === "enum" && !column.values.includes(value)) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`${column.name} filter must be ${column.values.join(", ")}`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
248
253
|
return value;
|
|
249
254
|
}
|
|
250
255
|
|
|
@@ -29,6 +29,23 @@ describe("repository", () => {
|
|
|
29
29
|
}
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
it("validates enum columns against declared values", () => {
|
|
33
|
+
const posts = repository({
|
|
34
|
+
table: "posts",
|
|
35
|
+
columns: {
|
|
36
|
+
status: {
|
|
37
|
+
type: "enum",
|
|
38
|
+
values: ["draft", "published"],
|
|
39
|
+
nullable: false,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
expect(posts.schema.parse({ status: "draft" })).toEqual({
|
|
44
|
+
status: "draft",
|
|
45
|
+
});
|
|
46
|
+
expect(() => posts.schema.parse({ status: "archived" })).toThrow();
|
|
47
|
+
});
|
|
48
|
+
|
|
32
49
|
it("exposes a row schema with and without meta", () => {
|
|
33
50
|
const users = repository({ table: "users", columns });
|
|
34
51
|
const at = new Date("2026-01-15T00:00:00.000Z");
|
|
@@ -38,6 +55,7 @@ describe("repository", () => {
|
|
|
38
55
|
name: "Ada",
|
|
39
56
|
email: "ada@example.com",
|
|
40
57
|
});
|
|
58
|
+
|
|
41
59
|
expect(
|
|
42
60
|
users.meta.parse({
|
|
43
61
|
...row("users_a", "Ada"),
|
|
@@ -10,31 +10,42 @@ import {
|
|
|
10
10
|
} from "./query.ts";
|
|
11
11
|
import type { ColumnType, DomainColumn } from "./sql.ts";
|
|
12
12
|
|
|
13
|
-
export type ColumnSpec =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
export type ColumnSpec =
|
|
14
|
+
| {
|
|
15
|
+
readonly type: Exclude<ColumnType, "enum">;
|
|
16
|
+
readonly nullable: boolean;
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
readonly type: "enum";
|
|
20
|
+
readonly values: readonly [string, ...string[]];
|
|
21
|
+
readonly nullable: boolean;
|
|
22
|
+
};
|
|
17
23
|
|
|
18
|
-
type
|
|
19
|
-
|
|
20
|
-
:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
type SpecValue<Spec extends ColumnSpec> = Spec extends {
|
|
25
|
+
type: "enum";
|
|
26
|
+
values: readonly (infer Value)[];
|
|
27
|
+
}
|
|
28
|
+
? Value
|
|
29
|
+
: Spec["type"] extends "text"
|
|
30
|
+
? string
|
|
31
|
+
: Spec["type"] extends "boolean"
|
|
32
|
+
? boolean
|
|
33
|
+
: Spec["type"] extends "timestamptz"
|
|
34
|
+
? Date
|
|
35
|
+
: Spec["type"] extends "integer"
|
|
36
|
+
? number
|
|
37
|
+
: Spec["type"] extends "bigint"
|
|
29
38
|
? string
|
|
30
|
-
:
|
|
31
|
-
?
|
|
32
|
-
:
|
|
39
|
+
: Spec["type"] extends "numeric"
|
|
40
|
+
? string
|
|
41
|
+
: Spec["type"] extends "jsonb"
|
|
42
|
+
? unknown
|
|
43
|
+
: never;
|
|
33
44
|
|
|
34
45
|
export type DomainRow<Columns extends Record<string, ColumnSpec>> = {
|
|
35
46
|
[Key in keyof Columns]: Columns[Key]["nullable"] extends true
|
|
36
|
-
?
|
|
37
|
-
:
|
|
47
|
+
? SpecValue<Columns[Key]> | null
|
|
48
|
+
: SpecValue<Columns[Key]>;
|
|
38
49
|
};
|
|
39
50
|
|
|
40
51
|
export type RepositoryRow<Columns extends Record<string, ColumnSpec>> = {
|
|
@@ -133,11 +144,21 @@ export function repository<
|
|
|
133
144
|
}): Repository<Columns> &
|
|
134
145
|
(List extends false ? object : { list: ListProcessor<Columns, List> }) &
|
|
135
146
|
(Get extends true ? { get: GetProcessor<Columns> } : object) {
|
|
136
|
-
const columns = Object.entries(input.columns).map(
|
|
137
|
-
name,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
147
|
+
const columns = Object.entries(input.columns).map(
|
|
148
|
+
([name, column]): DomainColumn =>
|
|
149
|
+
column.type === "enum"
|
|
150
|
+
? {
|
|
151
|
+
name,
|
|
152
|
+
type: "enum",
|
|
153
|
+
nullable: column.nullable,
|
|
154
|
+
values: column.values,
|
|
155
|
+
}
|
|
156
|
+
: {
|
|
157
|
+
name,
|
|
158
|
+
type: column.type,
|
|
159
|
+
nullable: column.nullable,
|
|
160
|
+
},
|
|
161
|
+
);
|
|
141
162
|
const shape = rowSchemas(columns);
|
|
142
163
|
const base: Repository<Columns> = {
|
|
143
164
|
table: input.table,
|
|
@@ -240,7 +261,7 @@ function listFilters(
|
|
|
240
261
|
function rowSchemas(columns: readonly DomainColumn[]) {
|
|
241
262
|
const domain: Record<string, z.ZodType> = {};
|
|
242
263
|
for (const column of columns) {
|
|
243
|
-
domain[column.name] = columnSchema(column
|
|
264
|
+
domain[column.name] = columnSchema(column);
|
|
244
265
|
}
|
|
245
266
|
return {
|
|
246
267
|
schema: z.object(domain),
|
|
@@ -256,20 +277,22 @@ function rowSchemas(columns: readonly DomainColumn[]) {
|
|
|
256
277
|
};
|
|
257
278
|
}
|
|
258
279
|
|
|
259
|
-
function columnSchema(
|
|
280
|
+
function columnSchema(column: DomainColumn) {
|
|
260
281
|
const schema =
|
|
261
|
-
type === "boolean"
|
|
282
|
+
column.type === "boolean"
|
|
262
283
|
? z.boolean()
|
|
263
|
-
: type === "timestamptz"
|
|
284
|
+
: column.type === "timestamptz"
|
|
264
285
|
? z.date()
|
|
265
|
-
: type === "integer"
|
|
286
|
+
: column.type === "integer"
|
|
266
287
|
? z.number().int()
|
|
267
|
-
: type === "bigint" || type === "numeric"
|
|
288
|
+
: column.type === "bigint" || column.type === "numeric"
|
|
268
289
|
? z.string()
|
|
269
|
-
: type === "jsonb"
|
|
290
|
+
: column.type === "jsonb"
|
|
270
291
|
? z.unknown()
|
|
271
|
-
:
|
|
272
|
-
|
|
292
|
+
: column.type === "enum"
|
|
293
|
+
? z.enum(column.values)
|
|
294
|
+
: z.string();
|
|
295
|
+
return column.nullable ? schema.nullable() : schema;
|
|
273
296
|
}
|
|
274
297
|
|
|
275
298
|
function builtQuerySort(sort: string | undefined) {
|
|
@@ -75,4 +75,37 @@ describe("readTables", () => {
|
|
|
75
75
|
]),
|
|
76
76
|
).toThrow("users.photo has no TypeScript type");
|
|
77
77
|
});
|
|
78
|
+
|
|
79
|
+
it("reads create type enums and columns that use them", () => {
|
|
80
|
+
const tables = readTables([
|
|
81
|
+
{
|
|
82
|
+
file: "0001_status.sql",
|
|
83
|
+
sql: `
|
|
84
|
+
create type post_status as enum ('draft', 'published');
|
|
85
|
+
create table posts (${owned}, status post_status not null);
|
|
86
|
+
alter type post_status add value 'archived';
|
|
87
|
+
alter table posts add column visibility post_status;
|
|
88
|
+
`,
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
expect(tables).toEqual([
|
|
92
|
+
{
|
|
93
|
+
name: "posts",
|
|
94
|
+
columns: [
|
|
95
|
+
{
|
|
96
|
+
name: "status",
|
|
97
|
+
type: "enum",
|
|
98
|
+
nullable: false,
|
|
99
|
+
values: ["draft", "published", "archived"],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "visibility",
|
|
103
|
+
type: "enum",
|
|
104
|
+
nullable: true,
|
|
105
|
+
values: ["draft", "published", "archived"],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
78
111
|
});
|