@ketvietlab/ketjs 0.1.2 → 0.1.3
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 +1 -1
- package/README.md +8 -2
- package/dist/agent/capabilities.d.ts +1 -3
- package/dist/agent/capabilities.d.ts.map +1 -1
- package/dist/agent/capabilities.js +10 -1
- package/dist/agent/capabilities.js.map +1 -1
- package/dist/agent/permissions.d.ts +43 -1
- package/dist/agent/permissions.d.ts.map +1 -1
- package/dist/agent/permissions.js +50 -2
- package/dist/agent/permissions.js.map +1 -1
- package/dist/cli.js +246 -97
- package/dist/cli.js.map +1 -1
- package/dist/codegen/dts.d.ts.map +1 -1
- package/dist/codegen/dts.js +1 -0
- package/dist/codegen/dts.js.map +1 -1
- package/dist/data/changeset.d.ts +12 -0
- package/dist/data/changeset.d.ts.map +1 -1
- package/dist/data/changeset.js +73 -17
- package/dist/data/changeset.js.map +1 -1
- package/dist/data/expr.d.ts +16 -0
- package/dist/data/expr.d.ts.map +1 -1
- package/dist/data/expr.js +33 -5
- package/dist/data/expr.js.map +1 -1
- package/dist/data/fleet.d.ts +41 -1
- package/dist/data/fleet.d.ts.map +1 -1
- package/dist/data/fleet.js +100 -11
- package/dist/data/fleet.js.map +1 -1
- package/dist/data/migrate.d.ts +20 -0
- package/dist/data/migrate.d.ts.map +1 -1
- package/dist/data/migrate.js +86 -3
- package/dist/data/migrate.js.map +1 -1
- package/dist/data/physical.d.ts +5 -0
- package/dist/data/physical.d.ts.map +1 -0
- package/dist/data/physical.js +196 -0
- package/dist/data/physical.js.map +1 -0
- package/dist/data/pool.d.ts +2 -1
- package/dist/data/pool.d.ts.map +1 -1
- package/dist/data/pool.js +159 -31
- package/dist/data/pool.js.map +1 -1
- package/dist/data/query.d.ts +10 -1
- package/dist/data/query.d.ts.map +1 -1
- package/dist/data/query.js +136 -27
- package/dist/data/query.js.map +1 -1
- package/dist/data/sqlite.d.ts.map +1 -1
- package/dist/data/sqlite.js +350 -52
- package/dist/data/sqlite.js.map +1 -1
- package/dist/data/time.d.ts +13 -0
- package/dist/data/time.d.ts.map +1 -1
- package/dist/data/time.js +61 -17
- package/dist/data/time.js.map +1 -1
- package/dist/index.d.ts +43 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -16
- package/dist/index.js.map +1 -1
- package/dist/kernel/audit.d.ts +27 -0
- package/dist/kernel/audit.d.ts.map +1 -0
- package/dist/kernel/audit.js +67 -0
- package/dist/kernel/audit.js.map +1 -0
- package/dist/kernel/classification.d.ts +26 -0
- package/dist/kernel/classification.d.ts.map +1 -0
- package/dist/kernel/classification.js +73 -0
- package/dist/kernel/classification.js.map +1 -0
- package/dist/kernel/compose.d.ts +6 -2
- package/dist/kernel/compose.d.ts.map +1 -1
- package/dist/kernel/compose.js +269 -15
- package/dist/kernel/compose.js.map +1 -1
- package/dist/kernel/define.d.ts +2 -3
- package/dist/kernel/define.d.ts.map +1 -1
- package/dist/kernel/define.js +22 -12
- package/dist/kernel/define.js.map +1 -1
- package/dist/kernel/diff.d.ts.map +1 -1
- package/dist/kernel/diff.js +28 -4
- package/dist/kernel/diff.js.map +1 -1
- package/dist/kernel/graph.js +1 -1
- package/dist/kernel/graph.js.map +1 -1
- package/dist/kernel/i18n.d.ts +10 -1
- package/dist/kernel/i18n.d.ts.map +1 -1
- package/dist/kernel/i18n.js +34 -1
- package/dist/kernel/i18n.js.map +1 -1
- package/dist/kernel/layout.d.ts +97 -0
- package/dist/kernel/layout.d.ts.map +1 -1
- package/dist/kernel/layout.js +294 -12
- package/dist/kernel/layout.js.map +1 -1
- package/dist/kernel/menu.d.ts +33 -2
- package/dist/kernel/menu.d.ts.map +1 -1
- package/dist/kernel/menu.js +99 -7
- package/dist/kernel/menu.js.map +1 -1
- package/dist/kernel/modules.d.ts +3 -3
- package/dist/kernel/modules.d.ts.map +1 -1
- package/dist/kernel/modules.js +21 -21
- package/dist/kernel/modules.js.map +1 -1
- package/dist/kernel/permissions.d.ts +10 -0
- package/dist/kernel/permissions.d.ts.map +1 -0
- package/dist/kernel/permissions.js +365 -0
- package/dist/kernel/permissions.js.map +1 -0
- package/dist/kernel/schedule.d.ts +23 -0
- package/dist/kernel/schedule.d.ts.map +1 -0
- package/dist/kernel/schedule.js +83 -0
- package/dist/kernel/schedule.js.map +1 -0
- package/dist/kernel/types.d.ts.map +1 -1
- package/dist/kernel/types.js +5 -4
- package/dist/kernel/types.js.map +1 -1
- package/dist/kernel/workspace.d.ts +92 -14
- package/dist/kernel/workspace.d.ts.map +1 -1
- package/dist/kernel/workspace.js +128 -49
- package/dist/kernel/workspace.js.map +1 -1
- package/dist/pdf/assets/Inter-Bold.ttf +0 -0
- package/dist/pdf/assets/Inter-Regular.ttf +0 -0
- package/dist/pdf/assets/Inter-SemiBold.ttf +0 -0
- package/dist/pdf/assets/LICENSE.txt +92 -0
- package/dist/pdf/font.d.ts +10 -0
- package/dist/pdf/font.d.ts.map +1 -0
- package/dist/pdf/font.js +83 -0
- package/dist/pdf/font.js.map +1 -0
- package/dist/pdf/image.d.ts +8 -0
- package/dist/pdf/image.d.ts.map +1 -0
- package/dist/pdf/image.js +94 -0
- package/dist/pdf/image.js.map +1 -0
- package/dist/pdf/index.d.ts +15 -0
- package/dist/pdf/index.d.ts.map +1 -0
- package/dist/pdf/index.js +17 -0
- package/dist/pdf/index.js.map +1 -0
- package/dist/pdf/markup.d.ts +19 -0
- package/dist/pdf/markup.d.ts.map +1 -0
- package/dist/pdf/markup.js +146 -0
- package/dist/pdf/markup.js.map +1 -0
- package/dist/pdf/render.d.ts +14 -0
- package/dist/pdf/render.d.ts.map +1 -0
- package/dist/pdf/render.js +387 -0
- package/dist/pdf/render.js.map +1 -0
- package/dist/scaffold/index.js +6 -6
- package/dist/scaffold/index.js.map +1 -1
- package/dist/scaffold/templates/{app.test.ts.tmpl → deployment.test.ts.tmpl} +3 -3
- package/dist/scaffold/templates/ket.workspace.ts.tmpl +4 -6
- package/dist/scaffold/templates/module.ts.tmpl +1 -4
- package/dist/server/assets.d.ts +23 -0
- package/dist/server/assets.d.ts.map +1 -0
- package/dist/server/assets.js +97 -0
- package/dist/server/assets.js.map +1 -0
- package/dist/server/boot.d.ts +157 -38
- package/dist/server/boot.d.ts.map +1 -1
- package/dist/server/boot.js +360 -138
- package/dist/server/boot.js.map +1 -1
- package/dist/server/config.d.ts +30 -10
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +73 -17
- package/dist/server/config.js.map +1 -1
- package/dist/server/ctx.d.ts +10 -0
- package/dist/server/ctx.d.ts.map +1 -1
- package/dist/server/ctx.js +266 -48
- package/dist/server/ctx.js.map +1 -1
- package/dist/server/development.d.ts +10 -0
- package/dist/server/development.d.ts.map +1 -0
- package/dist/server/development.js +15 -0
- package/dist/server/development.js.map +1 -0
- package/dist/server/fn.d.ts +22 -3
- package/dist/server/fn.d.ts.map +1 -1
- package/dist/server/fn.js +116 -32
- package/dist/server/fn.js.map +1 -1
- package/dist/server/form.d.ts +33 -0
- package/dist/server/form.d.ts.map +1 -0
- package/dist/server/form.js +47 -0
- package/dist/server/form.js.map +1 -0
- package/dist/server/http.d.ts +55 -3
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +306 -22
- package/dist/server/http.js.map +1 -1
- package/dist/server/idem.d.ts +3 -2
- package/dist/server/idem.d.ts.map +1 -1
- package/dist/server/idem.js +12 -4
- package/dist/server/idem.js.map +1 -1
- package/dist/server/jobs.js +1 -1
- package/dist/server/jobs.js.map +1 -1
- package/dist/server/log/combinators.d.ts +42 -0
- package/dist/server/log/combinators.d.ts.map +1 -0
- package/dist/server/log/combinators.js +270 -0
- package/dist/server/log/combinators.js.map +1 -0
- package/dist/server/log/console.d.ts +12 -0
- package/dist/server/log/console.d.ts.map +1 -0
- package/dist/server/log/console.js +96 -0
- package/dist/server/log/console.js.map +1 -0
- package/dist/server/log/file.d.ts +11 -0
- package/dist/server/log/file.d.ts.map +1 -0
- package/dist/server/log/file.js +61 -0
- package/dist/server/log/file.js.map +1 -0
- package/dist/server/log/index.d.ts +23 -0
- package/dist/server/log/index.d.ts.map +1 -0
- package/dist/server/log/index.js +45 -0
- package/dist/server/log/index.js.map +1 -0
- package/dist/server/log/logger.d.ts +53 -0
- package/dist/server/log/logger.d.ts.map +1 -0
- package/dist/server/log/logger.js +94 -0
- package/dist/server/log/logger.js.map +1 -0
- package/dist/server/log/memory.d.ts +11 -0
- package/dist/server/log/memory.d.ts.map +1 -0
- package/dist/server/log/memory.js +25 -0
- package/dist/server/log/memory.js.map +1 -0
- package/dist/server/log/types.d.ts +112 -0
- package/dist/server/log/types.d.ts.map +1 -0
- package/dist/server/log/types.js +58 -0
- package/dist/server/log/types.js.map +1 -0
- package/dist/server/policy.d.ts +29 -0
- package/dist/server/policy.d.ts.map +1 -0
- package/dist/server/policy.js +29 -0
- package/dist/server/policy.js.map +1 -0
- package/dist/server/queue.d.ts +1 -1
- package/dist/server/queue.d.ts.map +1 -1
- package/dist/server/queue.js.map +1 -1
- package/dist/server/ratelimit.d.ts +48 -0
- package/dist/server/ratelimit.d.ts.map +1 -0
- package/dist/server/ratelimit.js +102 -0
- package/dist/server/ratelimit.js.map +1 -0
- package/dist/server/respond.d.ts.map +1 -1
- package/dist/server/respond.js +16 -1
- package/dist/server/respond.js.map +1 -1
- package/dist/server/runtime.d.ts +18 -3
- package/dist/server/runtime.d.ts.map +1 -1
- package/dist/server/runtime.js +32 -2
- package/dist/server/runtime.js.map +1 -1
- package/dist/server/schedule.d.ts +18 -0
- package/dist/server/schedule.d.ts.map +1 -0
- package/dist/server/schedule.js +70 -0
- package/dist/server/schedule.js.map +1 -0
- package/dist/server/sequence.d.ts +42 -0
- package/dist/server/sequence.d.ts.map +1 -0
- package/dist/server/sequence.js +97 -0
- package/dist/server/sequence.js.map +1 -0
- package/dist/server/session.d.ts +21 -0
- package/dist/server/session.d.ts.map +1 -1
- package/dist/server/session.js +84 -10
- package/dist/server/session.js.map +1 -1
- package/dist/server/sessionstore.d.ts +3 -1
- package/dist/server/sessionstore.d.ts.map +1 -1
- package/dist/server/sessionstore.js +7 -3
- package/dist/server/sessionstore.js.map +1 -1
- package/dist/server/storage/index.d.ts +1 -1
- package/dist/server/storage/index.d.ts.map +1 -1
- package/dist/server/storage/index.js +61 -1
- package/dist/server/storage/index.js.map +1 -1
- package/dist/server/storage/types.d.ts +8 -0
- package/dist/server/storage/types.d.ts.map +1 -1
- package/dist/server/storage/types.js +58 -0
- package/dist/server/storage/types.js.map +1 -1
- package/dist/server/tenants.d.ts +20 -18
- package/dist/server/tenants.d.ts.map +1 -1
- package/dist/server/tenants.js +133 -84
- package/dist/server/tenants.js.map +1 -1
- package/dist/server/transport/types.js +2 -2
- package/dist/server/transport/types.js.map +1 -1
- package/dist/server/worker.d.ts +19 -3
- package/dist/server/worker.d.ts.map +1 -1
- package/dist/server/worker.js +168 -21
- package/dist/server/worker.js.map +1 -1
- package/dist/testing.d.ts +26 -10
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +41 -14
- package/dist/testing.js.map +1 -1
- package/dist/theme/contracts.d.ts +14 -1
- package/dist/theme/contracts.d.ts.map +1 -1
- package/dist/theme/contracts.js +35 -6
- package/dist/theme/contracts.js.map +1 -1
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/joint-runtime.d.ts +33 -0
- package/dist/theme/joint-runtime.d.ts.map +1 -0
- package/dist/theme/joint-runtime.js +121 -0
- package/dist/theme/joint-runtime.js.map +1 -0
- package/dist/theme/joints.d.ts.map +1 -1
- package/dist/theme/joints.js +15 -97
- package/dist/theme/joints.js.map +1 -1
- package/dist/theme/ktl/compile.d.ts +4 -0
- package/dist/theme/ktl/compile.d.ts.map +1 -1
- package/dist/theme/ktl/compile.js +54 -1
- package/dist/theme/ktl/compile.js.map +1 -1
- package/dist/theme/ktl/parser.d.ts +4 -0
- package/dist/theme/ktl/parser.d.ts.map +1 -1
- package/dist/theme/ktl/parser.js +8 -0
- package/dist/theme/ktl/parser.js.map +1 -1
- package/dist/theme/render.d.ts +8 -0
- package/dist/theme/render.d.ts.map +1 -1
- package/dist/theme/render.js +88 -118
- package/dist/theme/render.js.map +1 -1
- package/dist/theme/tokens.d.ts +6 -0
- package/dist/theme/tokens.d.ts.map +1 -1
- package/dist/theme/tokens.js +6 -0
- package/dist/theme/tokens.js.map +1 -1
- package/dist/theme/viewmodel.d.ts +24 -0
- package/dist/theme/viewmodel.d.ts.map +1 -1
- package/dist/theme/viewmodel.js +75 -6
- package/dist/theme/viewmodel.js.map +1 -1
- package/dist/types.d.ts +304 -56
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -2
- package/dist/kernel/apps.d.ts +0 -43
- package/dist/kernel/apps.d.ts.map +0 -1
- package/dist/kernel/apps.js +0 -234
- package/dist/kernel/apps.js.map +0 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// What the deployment's data actually is, read off the manifest.
|
|
2
|
+
//
|
|
3
|
+
// An obligation to export or erase somebody's data cannot be met against a schema
|
|
4
|
+
// nobody has classified, and neither can a question as simple as "which columns
|
|
5
|
+
// would leak if this table went out the door". Both are answered here, from the
|
|
6
|
+
// same composed manifest the migration and the agent descriptor read, so the answer
|
|
7
|
+
// cannot drift from what the deployment actually runs.
|
|
8
|
+
//
|
|
9
|
+
// The inventory deliberately reports what is *not* classified as well. An inventory
|
|
10
|
+
// of only the fields somebody remembered to tag is the one thing worse than no
|
|
11
|
+
// inventory: it looks complete.
|
|
12
|
+
const typeOf = (field) => `${field.base === 'ref' ? `ref:${field.target}` : field.base}${field.optional ? '?' : ''}`;
|
|
13
|
+
export function classificationInventory(manifest) {
|
|
14
|
+
const personal = [];
|
|
15
|
+
const sensitive = [];
|
|
16
|
+
const unclassified = [];
|
|
17
|
+
let fields = 0;
|
|
18
|
+
for (const [model, def] of Object.entries(manifest.models).sort(([a], [b]) => a.localeCompare(b))) {
|
|
19
|
+
let classified = false;
|
|
20
|
+
for (const [field, spec] of Object.entries(def.fields).sort(([a], [b]) => a.localeCompare(b))) {
|
|
21
|
+
fields += 1;
|
|
22
|
+
if (!spec.personal && !spec.sensitive)
|
|
23
|
+
continue;
|
|
24
|
+
classified = true;
|
|
25
|
+
const entry = {
|
|
26
|
+
model,
|
|
27
|
+
field,
|
|
28
|
+
by: spec.by,
|
|
29
|
+
type: typeOf(spec),
|
|
30
|
+
personal: Boolean(spec.personal),
|
|
31
|
+
sensitive: Boolean(spec.sensitive),
|
|
32
|
+
};
|
|
33
|
+
if (entry.personal)
|
|
34
|
+
personal.push(entry);
|
|
35
|
+
if (entry.sensitive)
|
|
36
|
+
sensitive.push(entry);
|
|
37
|
+
}
|
|
38
|
+
if (!classified)
|
|
39
|
+
unclassified.push(model);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
personal,
|
|
43
|
+
sensitive,
|
|
44
|
+
unclassified,
|
|
45
|
+
counts: {
|
|
46
|
+
models: Object.keys(manifest.models).length,
|
|
47
|
+
fields,
|
|
48
|
+
personal: personal.length,
|
|
49
|
+
sensitive: sensitive.length,
|
|
50
|
+
unclassified: unclassified.length,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const row = (entry, width) => ` ${`${entry.model}.${entry.field}`.padEnd(width)} ${entry.type.padEnd(18)} by ${entry.by}`;
|
|
55
|
+
export function formatClassification(inventory) {
|
|
56
|
+
const all = [...inventory.personal, ...inventory.sensitive];
|
|
57
|
+
const width = Math.max(24, ...all.map((entry) => `${entry.model}.${entry.field}`.length));
|
|
58
|
+
const lines = [];
|
|
59
|
+
lines.push(`personal data (${inventory.counts.personal} field(s))`);
|
|
60
|
+
lines.push(...(inventory.personal.length ? inventory.personal.map((e) => row(e, width)) : [' none']));
|
|
61
|
+
lines.push('');
|
|
62
|
+
lines.push(`sensitive (${inventory.counts.sensitive} field(s)) — never leaves the system`);
|
|
63
|
+
lines.push(...(inventory.sensitive.length ? inventory.sensitive.map((e) => row(e, width)) : [' none']));
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push(`${inventory.counts.unclassified} of ${inventory.counts.models} model(s) classify no field at all`);
|
|
66
|
+
// Named rather than counted, because the list is the work queue.
|
|
67
|
+
if (inventory.unclassified.length) {
|
|
68
|
+
for (const model of inventory.unclassified)
|
|
69
|
+
lines.push(` ${model}`);
|
|
70
|
+
}
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=classification.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classification.js","sourceRoot":"","sources":["../../../../../../packages/ketjs/src/kernel/classification.ts"],"names":[],"mappings":"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"}
|
package/dist/kernel/compose.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import type { KetModule, Manifest } from '../types.ts';
|
|
1
|
+
import type { KetModule, Manifest, ModulePermissionsDef } from '../types.ts';
|
|
2
|
+
import type { RoleTemplateDef } from '../types.ts';
|
|
2
3
|
export declare function compose(modules: KetModule[], opts?: {
|
|
3
|
-
|
|
4
|
+
requiredRegions?: string[];
|
|
4
5
|
headless?: boolean;
|
|
6
|
+
requirePermissionCoverage?: boolean;
|
|
7
|
+
roleTemplates?: Record<string, RoleTemplateDef>;
|
|
8
|
+
modulePermissionDeclarations?: Record<string, ModulePermissionsDef>;
|
|
5
9
|
}): Manifest;
|
|
6
10
|
//# sourceMappingURL=compose.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../../../../../packages/ketjs/src/kernel/compose.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../../../../../packages/ketjs/src/kernel/compose.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAiB,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAK3F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAiDlD,wBAAgB,OAAO,CACrB,OAAO,EAAE,SAAS,EAAE,EACpB,IAAI,GAAE;IACJ,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,yBAAyB,CAAC,EAAE,OAAO,CAAA;IACnC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;IAC/C,4BAA4B,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;CAC/D,GACL,QAAQ,CAsqCV"}
|
package/dist/kernel/compose.js
CHANGED
|
@@ -7,7 +7,48 @@
|
|
|
7
7
|
import { topoSort } from './graph.js';
|
|
8
8
|
import { Diagnostics } from './errors.js';
|
|
9
9
|
import { parseType } from './types.js';
|
|
10
|
+
import { validateSchedule } from './schedule.js';
|
|
10
11
|
import { ambiguousRoutes, parseRoutePattern } from './routes.js';
|
|
12
|
+
import { tableNameFor } from '../data/migrate.js';
|
|
13
|
+
import { compilePermissionBundles } from './permissions.js';
|
|
14
|
+
const FIELD_KEYS = new Set(['type', 'personal', 'sensitive']);
|
|
15
|
+
/**
|
|
16
|
+
* Accept both field forms.
|
|
17
|
+
*
|
|
18
|
+
* The vocabulary is closed: a key nobody knows is a build error rather than a flag
|
|
19
|
+
* that silently does nothing, which is how a field ends up believed to be protected
|
|
20
|
+
* when it is not.
|
|
21
|
+
*/
|
|
22
|
+
function normalizeField(declared) {
|
|
23
|
+
if (typeof declared === 'string') {
|
|
24
|
+
return { ok: true, type: declared, personal: false, sensitive: false };
|
|
25
|
+
}
|
|
26
|
+
if (declared === null || typeof declared !== 'object') {
|
|
27
|
+
return { ok: false, code: 'E_BAD_FIELD', reason: 'a field is a type string or an object with a type' };
|
|
28
|
+
}
|
|
29
|
+
const unknown = Object.keys(declared).filter((k) => !FIELD_KEYS.has(k));
|
|
30
|
+
if (unknown.length) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
code: 'E_BAD_FIELD',
|
|
34
|
+
reason: `unknown field key(s) ${unknown.join(', ')} (accepted: ${[...FIELD_KEYS].join(', ')})`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (typeof declared.type !== 'string') {
|
|
38
|
+
return { ok: false, code: 'E_BAD_FIELD', reason: 'a field object needs a type string' };
|
|
39
|
+
}
|
|
40
|
+
for (const flag of ['personal', 'sensitive']) {
|
|
41
|
+
if (declared[flag] !== undefined && typeof declared[flag] !== 'boolean') {
|
|
42
|
+
return { ok: false, code: 'E_BAD_FIELD', reason: `"${flag}" must be a boolean` };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
type: declared.type,
|
|
48
|
+
personal: declared.personal === true,
|
|
49
|
+
sensitive: declared.sensitive === true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
11
52
|
const qualify = (mod, name) => `${mod}.${name}`;
|
|
12
53
|
const jointKey = (mod, name) => `${mod}:${name}`;
|
|
13
54
|
export function compose(modules, opts = {}) {
|
|
@@ -23,9 +64,20 @@ export function compose(modules, opts = {}) {
|
|
|
23
64
|
joints: {},
|
|
24
65
|
fills: [],
|
|
25
66
|
functions: {},
|
|
67
|
+
permissions: {
|
|
68
|
+
version: 1,
|
|
69
|
+
digest: '',
|
|
70
|
+
coverageRequired: false,
|
|
71
|
+
modules: {},
|
|
72
|
+
bundles: {},
|
|
73
|
+
functions: {},
|
|
74
|
+
exemptions: {},
|
|
75
|
+
roleTemplates: {},
|
|
76
|
+
},
|
|
26
77
|
jobs: {},
|
|
27
78
|
views: {},
|
|
28
|
-
|
|
79
|
+
reports: {},
|
|
80
|
+
regions: { required: [...(opts.requiredRegions ?? [])], provided: {} },
|
|
29
81
|
islands: {},
|
|
30
82
|
sections: {},
|
|
31
83
|
contentTypes: {},
|
|
@@ -35,27 +87,81 @@ export function compose(modules, opts = {}) {
|
|
|
35
87
|
assets: {},
|
|
36
88
|
styles: [],
|
|
37
89
|
routes: {},
|
|
90
|
+
routePrefixes: {},
|
|
38
91
|
patches: [],
|
|
39
92
|
messages: {},
|
|
40
93
|
};
|
|
94
|
+
for (const m of order) {
|
|
95
|
+
for (const raw of m.reserves ?? []) {
|
|
96
|
+
const prefix = raw.trim();
|
|
97
|
+
if (!prefix.startsWith('/') || !prefix.endsWith('/') || prefix.includes('{')) {
|
|
98
|
+
diag.add({
|
|
99
|
+
code: 'E_ROUTE_PREFIX',
|
|
100
|
+
module: m.name,
|
|
101
|
+
message: `"${m.name}" reserves invalid route prefix "${raw}"`,
|
|
102
|
+
hint: 'a reserved prefix is an absolute static path ending in /',
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const conflict = Object.entries(manifest.routePrefixes).find(([other]) => prefix.startsWith(other) || other.startsWith(prefix));
|
|
107
|
+
if (conflict) {
|
|
108
|
+
diag.add({
|
|
109
|
+
code: 'E_ROUTE_PREFIX_CLASH',
|
|
110
|
+
module: m.name,
|
|
111
|
+
message: `"${m.name}" and "${conflict[1]}" reserve overlapping prefixes "${prefix}" and "${conflict[0]}"`,
|
|
112
|
+
hint: 'one module must own the whole public namespace',
|
|
113
|
+
});
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
manifest.routePrefixes[prefix] = m.name;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const major = (version) => {
|
|
120
|
+
const match = /^(?:\^)?(\d+)(?:\.|$)/.exec(version.trim());
|
|
121
|
+
return match ? Number(match[1]) : null;
|
|
122
|
+
};
|
|
123
|
+
for (const m of order) {
|
|
124
|
+
for (const [dependency, range] of Object.entries(m.compatible ?? {})) {
|
|
125
|
+
if (!m.depends.includes(dependency)) {
|
|
126
|
+
diag.add({
|
|
127
|
+
code: 'E_MODULE_COMPATIBILITY_DEPENDENCY',
|
|
128
|
+
module: m.name,
|
|
129
|
+
message: `"${m.name}" declares compatibility with "${dependency}" without depending on it`,
|
|
130
|
+
hint: `add "${dependency}" to depends`,
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const actual = order.find((candidate) => candidate.name === dependency)?.version;
|
|
135
|
+
const wantedMajor = major(range);
|
|
136
|
+
const actualMajor = actual ? major(actual) : null;
|
|
137
|
+
const matches = range.startsWith('^')
|
|
138
|
+
? wantedMajor != null && actualMajor === wantedMajor
|
|
139
|
+
: actual === range;
|
|
140
|
+
if (!matches) {
|
|
141
|
+
diag.add({
|
|
142
|
+
code: 'E_MODULE_VERSION_SKEW',
|
|
143
|
+
module: m.name,
|
|
144
|
+
message: `"${m.name}" requires "${dependency}" ${range}, but the deployment has ${actual ?? 'nothing'}`,
|
|
145
|
+
hint: 'upgrade the extension and its contract owner together',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
41
150
|
for (const m of order) {
|
|
42
151
|
manifest.modules[m.name] = {
|
|
43
152
|
version: m.version,
|
|
44
153
|
kind: m.kind,
|
|
45
154
|
depends: [...m.depends],
|
|
46
|
-
app: m.app,
|
|
47
155
|
title: m.title,
|
|
48
156
|
summary: m.summary,
|
|
49
157
|
category: m.category,
|
|
50
|
-
install: m.install ?? 'manual',
|
|
51
|
-
removable: m.removable !== false,
|
|
52
158
|
};
|
|
53
159
|
}
|
|
54
160
|
// --- the served surface -------------------------------------------------
|
|
55
161
|
//
|
|
56
162
|
// Assets, stylesheets and routes are composed for the same reason models are:
|
|
57
|
-
// otherwise the
|
|
58
|
-
// file layout and going on serving it after that module
|
|
163
|
+
// otherwise the deployment hand-assembles them, which means knowing another module's
|
|
164
|
+
// file layout and going on serving it after that module leaves the composition.
|
|
59
165
|
//
|
|
60
166
|
// `order` is dependency order, so a module that extends another contributes its
|
|
61
167
|
// stylesheet after it and can override it. That ordering is the point.
|
|
@@ -93,6 +199,21 @@ export function compose(modules, opts = {}) {
|
|
|
93
199
|
});
|
|
94
200
|
continue;
|
|
95
201
|
}
|
|
202
|
+
const reservation = Object.entries(manifest.routePrefixes).find(([prefix]) => path.startsWith(prefix));
|
|
203
|
+
if (reservation) {
|
|
204
|
+
const [prefix, owner] = reservation;
|
|
205
|
+
const contribution = typeof make === 'function' ? null : make.through;
|
|
206
|
+
const allowed = m.name === owner || (contribution === owner && m.depends.includes(owner));
|
|
207
|
+
if (!allowed) {
|
|
208
|
+
diag.add({
|
|
209
|
+
code: 'E_ROUTE_RESERVED',
|
|
210
|
+
module: m.name,
|
|
211
|
+
message: `"${m.name}" claims "${path}", inside the prefix reserved by "${owner}"`,
|
|
212
|
+
hint: `depend on "${owner}" and use its published route factory for "${prefix}"`,
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
96
217
|
const taken = manifest.routes[path];
|
|
97
218
|
if (taken) {
|
|
98
219
|
diag.add({
|
|
@@ -117,13 +238,33 @@ export function compose(modules, opts = {}) {
|
|
|
117
238
|
manifest.routes[path] =
|
|
118
239
|
typeof make === 'function'
|
|
119
240
|
? { by: m.name, anonymous: false, make }
|
|
120
|
-
: {
|
|
241
|
+
: {
|
|
242
|
+
by: m.name,
|
|
243
|
+
anonymous: make.anonymous === true,
|
|
244
|
+
...(make.through ? { through: make.through } : {}),
|
|
245
|
+
...(make.contract ? { contract: make.contract } : {}),
|
|
246
|
+
make: make.handler,
|
|
247
|
+
};
|
|
121
248
|
}
|
|
122
249
|
}
|
|
123
250
|
// --- models -------------------------------------------------------------
|
|
251
|
+
const physicalTables = new Map();
|
|
124
252
|
for (const m of order) {
|
|
125
253
|
for (const [modelName, def] of Object.entries(m.models)) {
|
|
126
254
|
const key = qualify(m.name, modelName);
|
|
255
|
+
const physicalTable = tableNameFor(key);
|
|
256
|
+
const existingPhysicalModel = physicalTables.get(physicalTable);
|
|
257
|
+
if (existingPhysicalModel && existingPhysicalModel !== key) {
|
|
258
|
+
diag.add({
|
|
259
|
+
code: 'E_TABLE_NAME_COLLISION',
|
|
260
|
+
module: m.name,
|
|
261
|
+
message: `models "${existingPhysicalModel}" and "${key}" both map to table "${physicalTable}"`,
|
|
262
|
+
hint: 'rename one model or module so every composed model has a unique physical table',
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
physicalTables.set(physicalTable, key);
|
|
267
|
+
}
|
|
127
268
|
if (manifest.models[key]) {
|
|
128
269
|
diag.add({
|
|
129
270
|
code: 'E_MODEL_DUPLICATE',
|
|
@@ -162,7 +303,7 @@ export function compose(modules, opts = {}) {
|
|
|
162
303
|
fields['createdAt'] = { base: 'datetime', optional: true, by: '(timestamps)' };
|
|
163
304
|
fields['updatedAt'] = { base: 'datetime', optional: true, by: '(timestamps)' };
|
|
164
305
|
}
|
|
165
|
-
for (const [fname,
|
|
306
|
+
for (const [fname, declared] of Object.entries(def.fields ?? {})) {
|
|
166
307
|
if (def.timestamps && (fname === 'createdAt' || fname === 'updatedAt')) {
|
|
167
308
|
diag.add({
|
|
168
309
|
code: 'E_TIMESTAMP_FIELD_RESERVED',
|
|
@@ -172,6 +313,14 @@ export function compose(modules, opts = {}) {
|
|
|
172
313
|
});
|
|
173
314
|
continue;
|
|
174
315
|
}
|
|
316
|
+
// A field is a type string, or an object when it has more to say. The two
|
|
317
|
+
// forms compose to the same thing, so nothing downstream learns the difference.
|
|
318
|
+
const spec = normalizeField(declared);
|
|
319
|
+
if (!spec.ok) {
|
|
320
|
+
diag.add({ code: spec.code, module: m.name, message: `${key}.${fname}: ${spec.reason}` });
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const tspec = spec.type;
|
|
175
324
|
const t = parseType(tspec);
|
|
176
325
|
if (!t.ok) {
|
|
177
326
|
diag.add({ code: 'E_BAD_TYPE', module: m.name, message: `${key}.${fname}: ${t.reason}` });
|
|
@@ -186,7 +335,14 @@ export function compose(modules, opts = {}) {
|
|
|
186
335
|
});
|
|
187
336
|
continue;
|
|
188
337
|
}
|
|
189
|
-
fields[fname] = {
|
|
338
|
+
fields[fname] = {
|
|
339
|
+
base: t.base,
|
|
340
|
+
optional: t.optional,
|
|
341
|
+
target: t.target,
|
|
342
|
+
by: m.name,
|
|
343
|
+
...(spec.personal ? { personal: true } : {}),
|
|
344
|
+
...(spec.sensitive ? { sensitive: true } : {}),
|
|
345
|
+
};
|
|
190
346
|
}
|
|
191
347
|
const indexes = {};
|
|
192
348
|
for (const [indexName, index] of Object.entries(def.indexes ?? {})) {
|
|
@@ -225,6 +381,7 @@ export function compose(modules, opts = {}) {
|
|
|
225
381
|
owner: m.name,
|
|
226
382
|
scope: def.scope,
|
|
227
383
|
timestamps: def.timestamps === true,
|
|
384
|
+
append: def.append === true,
|
|
228
385
|
fields,
|
|
229
386
|
indexes,
|
|
230
387
|
};
|
|
@@ -252,7 +409,7 @@ export function compose(modules, opts = {}) {
|
|
|
252
409
|
});
|
|
253
410
|
continue;
|
|
254
411
|
}
|
|
255
|
-
for (const [fname,
|
|
412
|
+
for (const [fname, declared] of Object.entries(addl)) {
|
|
256
413
|
// Checked ahead of the collision below so the message names the real cause:
|
|
257
414
|
// these columns are the isolation boundary, not a name somebody took first.
|
|
258
415
|
if (fname === 'companyId' || fname === 'branchId') {
|
|
@@ -274,6 +431,15 @@ export function compose(modules, opts = {}) {
|
|
|
274
431
|
});
|
|
275
432
|
continue;
|
|
276
433
|
}
|
|
434
|
+
// Extension takes both field forms too. A bridge is exactly the module that
|
|
435
|
+
// adds a phone number to somebody else's partner, so it is exactly the module
|
|
436
|
+
// that has to be able to say the column is personal data.
|
|
437
|
+
const spec = normalizeField(declared);
|
|
438
|
+
if (!spec.ok) {
|
|
439
|
+
diag.add({ code: spec.code, module: m.name, message: `${target}.${fname}: ${spec.reason}` });
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const tspec = spec.type;
|
|
277
443
|
const t = parseType(tspec);
|
|
278
444
|
if (!t.ok) {
|
|
279
445
|
diag.add({ code: 'E_BAD_TYPE', module: m.name, message: `${target}.${fname}: ${t.reason}` });
|
|
@@ -290,7 +456,14 @@ export function compose(modules, opts = {}) {
|
|
|
290
456
|
});
|
|
291
457
|
continue;
|
|
292
458
|
}
|
|
293
|
-
model.fields[fname] = {
|
|
459
|
+
model.fields[fname] = {
|
|
460
|
+
base: t.base,
|
|
461
|
+
optional: t.optional,
|
|
462
|
+
target: t.target,
|
|
463
|
+
by: m.name,
|
|
464
|
+
...(spec.personal ? { personal: true } : {}),
|
|
465
|
+
...(spec.sensitive ? { sensitive: true } : {}),
|
|
466
|
+
};
|
|
294
467
|
}
|
|
295
468
|
}
|
|
296
469
|
}
|
|
@@ -436,7 +609,7 @@ export function compose(modules, opts = {}) {
|
|
|
436
609
|
diag.add({
|
|
437
610
|
code: 'E_FILL_UNKNOWN_JOINT',
|
|
438
611
|
module: m.name,
|
|
439
|
-
message: `fills joint "${key}", which no
|
|
612
|
+
message: `fills joint "${key}", which no composed module publishes`,
|
|
440
613
|
hint: near.length
|
|
441
614
|
? `did you mean "${near[0]}"?`
|
|
442
615
|
: `published joints: ${Object.keys(manifest.joints).join(', ') || '(none)'}`,
|
|
@@ -477,7 +650,7 @@ export function compose(modules, opts = {}) {
|
|
|
477
650
|
diag.add({
|
|
478
651
|
code: 'E_OMIT_UNKNOWN_JOINT',
|
|
479
652
|
module: m.name,
|
|
480
|
-
message: `omits joint "${key}", which no
|
|
653
|
+
message: `omits joint "${key}", which no composed module publishes`,
|
|
481
654
|
hint: `published joints: ${Object.keys(manifest.joints).join(', ') || '(none)'}`,
|
|
482
655
|
});
|
|
483
656
|
continue;
|
|
@@ -544,6 +717,55 @@ export function compose(modules, opts = {}) {
|
|
|
544
717
|
};
|
|
545
718
|
}
|
|
546
719
|
}
|
|
720
|
+
manifest.permissions = compilePermissionBundles(order, manifest, {
|
|
721
|
+
requireCoverage: opts.requirePermissionCoverage,
|
|
722
|
+
roleTemplates: opts.roleTemplates,
|
|
723
|
+
moduleDeclarations: opts.modulePermissionDeclarations,
|
|
724
|
+
});
|
|
725
|
+
// --- printable reports ---------------------------------------------------
|
|
726
|
+
for (const m of order) {
|
|
727
|
+
for (const [name, def] of Object.entries(m.reports ?? {})) {
|
|
728
|
+
const id = qualify(m.name, name);
|
|
729
|
+
if (!/^[a-z][a-zA-Z0-9_]*$/.test(name)) {
|
|
730
|
+
diag.add({ code: 'E_REPORT_NAME', module: m.name, message: `invalid report name "${name}"` });
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (!manifest.models[def.target]) {
|
|
734
|
+
diag.add({
|
|
735
|
+
code: 'E_REPORT_UNKNOWN_MODEL',
|
|
736
|
+
module: m.name,
|
|
737
|
+
message: `report "${id}" targets unknown model "${def.target}"`,
|
|
738
|
+
});
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
const source = manifest.functions[def.source];
|
|
742
|
+
if (!source) {
|
|
743
|
+
diag.add({
|
|
744
|
+
code: 'E_REPORT_UNKNOWN_SOURCE',
|
|
745
|
+
module: m.name,
|
|
746
|
+
message: `report "${id}" uses unknown function "${def.source}"`,
|
|
747
|
+
});
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
if (source.effects.some((effect) => effect.startsWith('write:') || effect.startsWith('enqueue:'))) {
|
|
751
|
+
diag.add({
|
|
752
|
+
code: 'E_REPORT_SOURCE_WRITES',
|
|
753
|
+
module: m.name,
|
|
754
|
+
message: `report "${id}" source "${def.source}" is not read-only`,
|
|
755
|
+
});
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (!def.template.trim()) {
|
|
759
|
+
diag.add({
|
|
760
|
+
code: 'E_REPORT_TEMPLATE_EMPTY',
|
|
761
|
+
module: m.name,
|
|
762
|
+
message: `report "${id}" has no template`,
|
|
763
|
+
});
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
manifest.reports[id] = { ...def, by: m.name, id };
|
|
767
|
+
}
|
|
768
|
+
}
|
|
547
769
|
// --- background jobs -----------------------------------------------------
|
|
548
770
|
//
|
|
549
771
|
// Jobs run later and often on another process, but they touch the same data.
|
|
@@ -590,6 +812,37 @@ export function compose(modules, opts = {}) {
|
|
|
590
812
|
if (!parsed.ok)
|
|
591
813
|
diag.add({ code: 'E_BAD_TYPE', module: m.name, message: `${key} input ${input}: ${parsed.reason}` });
|
|
592
814
|
}
|
|
815
|
+
if (def.schedule) {
|
|
816
|
+
try {
|
|
817
|
+
validateSchedule(def.schedule);
|
|
818
|
+
}
|
|
819
|
+
catch (error) {
|
|
820
|
+
diag.add({
|
|
821
|
+
code: 'E_BAD_SCHEDULE',
|
|
822
|
+
module: m.name,
|
|
823
|
+
message: `job "${key}": ${error.message}`,
|
|
824
|
+
hint: error.hint ?? null,
|
|
825
|
+
});
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
// Nobody is there to supply arguments to a schedule, so a required input is
|
|
829
|
+
// a job that can only ever fail validation at three in the morning.
|
|
830
|
+
const required = Object.entries(def.input ?? {})
|
|
831
|
+
.filter(([, spec]) => {
|
|
832
|
+
const parsed = parseType(spec);
|
|
833
|
+
return parsed.ok && !parsed.optional;
|
|
834
|
+
})
|
|
835
|
+
.map(([name]) => name);
|
|
836
|
+
if (required.length) {
|
|
837
|
+
diag.add({
|
|
838
|
+
code: 'E_SCHEDULED_JOB_INPUT',
|
|
839
|
+
module: m.name,
|
|
840
|
+
message: `scheduled job "${key}" requires input ${required.join(', ')}`,
|
|
841
|
+
hint: 'a schedule enqueues no arguments — make them optional, or drop the schedule',
|
|
842
|
+
});
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
593
846
|
for (const effect of def.effects ?? []) {
|
|
594
847
|
// Enqueue targets are validated after every job has been collected, so a
|
|
595
848
|
// producer may refer to a job contributed later in dependency order.
|
|
@@ -627,6 +880,7 @@ export function compose(modules, opts = {}) {
|
|
|
627
880
|
idempotent: true,
|
|
628
881
|
maxAttempts,
|
|
629
882
|
timeoutMs,
|
|
883
|
+
...(def.schedule ? { schedule: def.schedule } : {}),
|
|
630
884
|
};
|
|
631
885
|
}
|
|
632
886
|
}
|
|
@@ -934,7 +1188,7 @@ export function compose(modules, opts = {}) {
|
|
|
934
1188
|
});
|
|
935
1189
|
}
|
|
936
1190
|
}
|
|
937
|
-
// --- theme <->
|
|
1191
|
+
// --- theme <-> deployment region contract --------------------------------
|
|
938
1192
|
for (const m of order) {
|
|
939
1193
|
for (const r of m.provides)
|
|
940
1194
|
(manifest.regions.provided[r] ??= []).push(m.name);
|
|
@@ -947,7 +1201,7 @@ export function compose(modules, opts = {}) {
|
|
|
947
1201
|
if (!manifest.regions.required.includes(r))
|
|
948
1202
|
manifest.regions.required.push(r);
|
|
949
1203
|
}
|
|
950
|
-
// A headless
|
|
1204
|
+
// A headless deployment renders nothing, so the region contract does not apply to it.
|
|
951
1205
|
// Requirements are still recorded, so adding a theme later checks them.
|
|
952
1206
|
for (const r of opts.headless ? [] : manifest.regions.required) {
|
|
953
1207
|
if (!manifest.regions.provided[r]) {
|