@isahaq/kosh-server 0.0.0-stage → 0.1.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.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -2
  3. package/dist/api/client.d.ts +18 -0
  4. package/dist/api/client.d.ts.map +1 -0
  5. package/dist/api/client.js +380 -0
  6. package/dist/api/client.js.map +1 -0
  7. package/dist/api/idempotency.d.ts +23 -0
  8. package/dist/api/idempotency.d.ts.map +1 -0
  9. package/dist/api/idempotency.js +52 -0
  10. package/dist/api/idempotency.js.map +1 -0
  11. package/dist/api/metadata.d.ts +65 -0
  12. package/dist/api/metadata.d.ts.map +1 -0
  13. package/dist/api/metadata.js +88 -0
  14. package/dist/api/metadata.js.map +1 -0
  15. package/dist/api/navigation.d.ts +46 -0
  16. package/dist/api/navigation.d.ts.map +1 -0
  17. package/dist/api/navigation.js +131 -0
  18. package/dist/api/navigation.js.map +1 -0
  19. package/dist/api/openapi.d.ts +22 -0
  20. package/dist/api/openapi.d.ts.map +1 -0
  21. package/dist/api/openapi.js +349 -0
  22. package/dist/api/openapi.js.map +1 -0
  23. package/dist/api/rest.d.ts +28 -0
  24. package/dist/api/rest.d.ts.map +1 -0
  25. package/dist/api/rest.js +435 -0
  26. package/dist/api/rest.js.map +1 -0
  27. package/dist/api/views.d.ts +67 -0
  28. package/dist/api/views.d.ts.map +1 -0
  29. package/dist/api/views.js +307 -0
  30. package/dist/api/views.js.map +1 -0
  31. package/dist/audit.d.ts +21 -0
  32. package/dist/audit.d.ts.map +1 -0
  33. package/dist/audit.js +26 -0
  34. package/dist/audit.js.map +1 -0
  35. package/dist/auth/admins.d.ts +26 -0
  36. package/dist/auth/admins.d.ts.map +1 -0
  37. package/dist/auth/admins.js +70 -0
  38. package/dist/auth/admins.js.map +1 -0
  39. package/dist/auth/base.d.ts +12 -0
  40. package/dist/auth/base.d.ts.map +1 -0
  41. package/dist/auth/base.js +192 -0
  42. package/dist/auth/base.js.map +1 -0
  43. package/dist/auth/crypto.d.ts +20 -0
  44. package/dist/auth/crypto.d.ts.map +1 -0
  45. package/dist/auth/crypto.js +100 -0
  46. package/dist/auth/crypto.js.map +1 -0
  47. package/dist/auth/install.d.ts +24 -0
  48. package/dist/auth/install.d.ts.map +1 -0
  49. package/dist/auth/install.js +18 -0
  50. package/dist/auth/install.js.map +1 -0
  51. package/dist/auth/routes.d.ts +20 -0
  52. package/dist/auth/routes.d.ts.map +1 -0
  53. package/dist/auth/routes.js +56 -0
  54. package/dist/auth/routes.js.map +1 -0
  55. package/dist/auth/service.d.ts +108 -0
  56. package/dist/auth/service.d.ts.map +1 -0
  57. package/dist/auth/service.js +477 -0
  58. package/dist/auth/service.js.map +1 -0
  59. package/dist/errors.d.ts +21 -0
  60. package/dist/errors.d.ts.map +1 -0
  61. package/dist/errors.js +78 -0
  62. package/dist/errors.js.map +1 -0
  63. package/dist/identity.d.ts +49 -0
  64. package/dist/identity.d.ts.map +1 -0
  65. package/dist/identity.js +58 -0
  66. package/dist/identity.js.map +1 -0
  67. package/dist/index.d.ts +26 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +26 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/orm.d.ts +14 -0
  72. package/dist/orm.d.ts.map +1 -0
  73. package/dist/orm.js +12 -0
  74. package/dist/orm.js.map +1 -0
  75. package/dist/registry.d.ts +19 -0
  76. package/dist/registry.d.ts.map +1 -0
  77. package/dist/registry.js +26 -0
  78. package/dist/registry.js.map +1 -0
  79. package/dist/server.d.ts +93 -0
  80. package/dist/server.d.ts.map +1 -0
  81. package/dist/server.js +223 -0
  82. package/dist/server.js.map +1 -0
  83. package/dist/telemetry.d.ts +23 -0
  84. package/dist/telemetry.d.ts.map +1 -0
  85. package/dist/telemetry.js +38 -0
  86. package/dist/telemetry.js.map +1 -0
  87. package/package.json +63 -3
@@ -0,0 +1,88 @@
1
+ const IMPLICIT_READONLY = new Set([
2
+ 'id',
3
+ 'createdAt',
4
+ 'createdBy',
5
+ 'updatedAt',
6
+ 'updatedBy',
7
+ 'displayName',
8
+ ]);
9
+ /** `res.partner` → `ResPartner`. */
10
+ export function schemaName(model) {
11
+ return model
12
+ .split(/[._]/)
13
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
14
+ .join('');
15
+ }
16
+ /** The API metadata of every model of the registry. */
17
+ export function apiModels(registry) {
18
+ const out = new Map();
19
+ for (const name of registry.models.keys()) {
20
+ const description = registry.describe(name);
21
+ const descriptors = registry.get(name).model.fields;
22
+ const fields = [];
23
+ const transitions = [];
24
+ const transitionSpecs = [];
25
+ for (const [key, d] of Object.entries(description.fields)) {
26
+ const raw = descriptors[key] ?? {};
27
+ const selection = raw['selection'];
28
+ if (d.kind === 'state')
29
+ for (const [t, spec] of Object.entries(raw['transitions'] ?? {})) {
30
+ transitions.push(t);
31
+ transitionSpecs.push({
32
+ name: t,
33
+ field: key,
34
+ from: Array.isArray(spec['from']) ? spec['from'] : [],
35
+ to: String(spec['to'] ?? ''),
36
+ requires: typeof spec['requires'] === 'string' ? spec['requires'] : null,
37
+ });
38
+ }
39
+ const computedReadonly = d.computed && raw['inverse'] === undefined;
40
+ fields.push({
41
+ key,
42
+ kind: d.kind,
43
+ label: d.label,
44
+ required: d.required,
45
+ readonly: d.readonly || computedReadonly || IMPLICIT_READONLY.has(key) || d.kind === 'state',
46
+ stored: d.column !== null,
47
+ requiredOnCreate: d.required &&
48
+ !d.hasDefault &&
49
+ !d.computed &&
50
+ d.sequence === null &&
51
+ !IMPLICIT_READONLY.has(key),
52
+ target: typeof d['target'] === 'string' ? d['target'] : null,
53
+ options: Array.isArray(selection)
54
+ ? selection
55
+ : null,
56
+ translate: d.translate,
57
+ });
58
+ }
59
+ out.set(name, {
60
+ name,
61
+ label: description.label,
62
+ schemaName: schemaName(name),
63
+ fields,
64
+ byKey: new Map(fields.map((f) => [f.key, f])),
65
+ transitions,
66
+ transitionSpecs,
67
+ onchange: [...description.onchange],
68
+ recName: recNameOf(registry, name, fields),
69
+ });
70
+ }
71
+ return out;
72
+ }
73
+ function recNameOf(registry, name, fields) {
74
+ const declared = registry.get(name).model.recName;
75
+ const key = typeof declared === 'string' ? declared : 'name';
76
+ const field = fields.find((f) => f.key === key);
77
+ return field?.stored === true && (field.kind === 'char' || field.kind === 'text') ? key : null;
78
+ }
79
+ /** Fields listed by default: stored, not binary, not x2many. */
80
+ export function defaultFields(model) {
81
+ return model.fields.filter((f) => f.stored && f.kind !== 'binary' && f.kind !== 'one2many' && f.kind !== 'many2many');
82
+ }
83
+ /** The models the generated API serves: every installed model except the modules' `privateModels`. */
84
+ export function publicApiModels(registry) {
85
+ const hidden = new Set(registry.loaded.modules.flatMap((m) => m.privateModels ?? []));
86
+ return new Map([...apiModels(registry.registry)].filter(([name]) => !hidden.has(name)));
87
+ }
88
+ //# sourceMappingURL=metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../../src/api/metadata.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Apps, menus and actions per user (RFC 0003 §5.1 "visibility"): installing a module shows nothing; a user sees an
3
+ * app, menu or action only with an access level of its module, or a group / permission it names in `visibleTo`, and
4
+ * an action on a model only with read access to it. Administrators see everything. The server filters the tree and
5
+ * refuses actions it would not show: the UI hiding is a convenience, never the control.
6
+ *
7
+ * - `GET /api/v1/apps` the launcher
8
+ * - `GET /api/v1/menus` the menu tree (optionally `?app=`)
9
+ * - `GET /api/v1/actions/:id` one action, 403 when not visible
10
+ */
11
+ import type { KoshModule, LoadedModules, MenuSpec } from '@isahaq/kosh-modules-sdk';
12
+ import type { FastifyInstance } from 'fastify';
13
+ import type { Env } from '../orm.js';
14
+ /** A visible menu entry with its visible children. */
15
+ export interface MenuNode {
16
+ readonly id: string;
17
+ readonly label: string;
18
+ readonly app?: string;
19
+ readonly action?: string;
20
+ readonly sequence: number;
21
+ readonly children: MenuNode[];
22
+ }
23
+ /** True when the user holds an access level of the module (or is an administrator). */
24
+ export declare function moduleVisible(env: Env, module: KoshModule): boolean;
25
+ /** Navigation for a loaded module set. */
26
+ export declare class Navigation {
27
+ #private;
28
+ constructor(loaded: LoadedModules);
29
+ /** The action spec, when the user may open it. */
30
+ actionVisible(env: Env, id: string): boolean;
31
+ menuVisible(env: Env, menu: MenuSpec & {
32
+ readonly module: string;
33
+ }): boolean;
34
+ /** The visible tree: hidden parents hide their children; entries without an action and visible children go. */
35
+ menus(env: Env, app?: string): MenuNode[];
36
+ /** The launcher: apps of modules the user has an access level of. */
37
+ apps(env: Env): {
38
+ code: string;
39
+ label: string;
40
+ icon?: string;
41
+ sequence: number;
42
+ }[];
43
+ /** Registers the routes. */
44
+ register(app: FastifyInstance): void;
45
+ }
46
+ //# sourceMappingURL=navigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/api/navigation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAErC,sDAAsD;AACtD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,EAAE,CAAC;CAC/B;AAgBD,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAEnE;AAYD,0CAA0C;AAC1C,qBAAa,UAAU;;IAGrB,YAAY,MAAM,EAAE,aAAa,EAEhC;IAMD,kDAAkD;IAClD,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAgB3C;IAED,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,GAAG;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAO3E;IAED,+GAA+G;IAC/G,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,QAAQ,EAAE,CAsBxC;IAQD,qEAAqE;IACrE,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAajF;IAED,4BAA4B;IAC5B,QAAQ,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CA0BnC;CACF"}
@@ -0,0 +1,131 @@
1
+ import { HttpProblem } from '../errors.js';
2
+ function holds(env, code) {
3
+ if (env.groups.has(code))
4
+ return true;
5
+ try {
6
+ return env.access.has(code); // a feature permission (by role, grant or administrator)
7
+ }
8
+ catch {
9
+ return false; // not a declared permission: a group the user does not hold
10
+ }
11
+ }
12
+ /** The groups of a module's access levels. */
13
+ function levelGroups(module) {
14
+ return Object.values(module.accessLevels).flatMap((level) => [...level.groups]);
15
+ }
16
+ /** True when the user holds an access level of the module (or is an administrator). */
17
+ export function moduleVisible(env, module) {
18
+ return env.administrator || levelGroups(module).some((g) => env.groups.has(g));
19
+ }
20
+ function actionModel(spec) {
21
+ const model = spec?.model;
22
+ return typeof model === 'string' ? model : undefined;
23
+ }
24
+ function visibleTo(spec) {
25
+ const codes = spec?.visibleTo;
26
+ return Array.isArray(codes) ? codes : undefined;
27
+ }
28
+ /** Navigation for a loaded module set. */
29
+ export class Navigation {
30
+ #loaded;
31
+ constructor(loaded) {
32
+ this.#loaded = loaded;
33
+ }
34
+ #module(name) {
35
+ return this.#loaded.byName.get(name);
36
+ }
37
+ /** The action spec, when the user may open it. */
38
+ actionVisible(env, id) {
39
+ const owner = this.#loaded.actions.get(id);
40
+ if (owner === undefined)
41
+ return false;
42
+ if (env.administrator)
43
+ return true;
44
+ const module = this.#module(owner);
45
+ const spec = module.actions?.[id];
46
+ const codes = visibleTo(spec);
47
+ if (codes !== undefined ? !codes.some((c) => holds(env, c)) : !moduleVisible(env, module))
48
+ return false;
49
+ const model = actionModel(spec);
50
+ if (model === undefined)
51
+ return true;
52
+ try {
53
+ return env.access.can(model, 'read');
54
+ }
55
+ catch {
56
+ return false; // a model that is not installed
57
+ }
58
+ }
59
+ menuVisible(env, menu) {
60
+ if (env.administrator)
61
+ return true;
62
+ const own = menu.visibleTo !== undefined
63
+ ? menu.visibleTo.some((c) => holds(env, c))
64
+ : moduleVisible(env, this.#module(menu.module));
65
+ return own && (menu.action === undefined || this.actionVisible(env, menu.action));
66
+ }
67
+ /** The visible tree: hidden parents hide their children; entries without an action and visible children go. */
68
+ menus(env, app) {
69
+ const all = this.#loaded.menus.filter((m) => app === undefined || this.#appOf(m) === app);
70
+ const byParent = new Map();
71
+ for (const m of all) {
72
+ const parent = m.parent !== undefined && all.some((p) => p.id === m.parent) ? m.parent : undefined;
73
+ byParent.set(parent, [...(byParent.get(parent) ?? []), m]);
74
+ }
75
+ const build = (parent) => (byParent.get(parent) ?? [])
76
+ .filter((m) => this.menuVisible(env, m))
77
+ .map((m) => ({
78
+ id: m.id,
79
+ label: m.label,
80
+ ...(this.#appOf(m) === undefined ? {} : { app: this.#appOf(m) }),
81
+ ...(m.action === undefined ? {} : { action: m.action }),
82
+ sequence: m.sequence ?? 10,
83
+ children: build(m.id),
84
+ }))
85
+ .filter((m) => m.action !== undefined || m.children.length > 0)
86
+ .sort((a, b) => a.sequence - b.sequence || a.label.localeCompare(b.label));
87
+ return build(undefined);
88
+ }
89
+ #appOf(menu) {
90
+ if (menu.app !== undefined)
91
+ return menu.app;
92
+ const parent = this.#loaded.menus.find((m) => m.id === menu.parent);
93
+ return parent === undefined ? this.#module(menu.module).app?.code : this.#appOf(parent);
94
+ }
95
+ /** The launcher: apps of modules the user has an access level of. */
96
+ apps(env) {
97
+ return this.#loaded.modules
98
+ .filter((m) => m.app !== undefined && moduleVisible(env, m))
99
+ .map((m) => {
100
+ const app = m.app;
101
+ return {
102
+ code: app.code,
103
+ label: app.label,
104
+ ...(app.icon === undefined ? {} : { icon: app.icon }),
105
+ sequence: app.sequence ?? 10,
106
+ };
107
+ })
108
+ .sort((a, b) => a.sequence - b.sequence || a.label.localeCompare(b.label));
109
+ }
110
+ /** Registers the routes. */
111
+ register(app) {
112
+ app.get('/api/v1/apps', async (request) => request.env(async (env) => ({ apps: this.apps(env) })));
113
+ app.get('/api/v1/menus', async (request) => {
114
+ const q = (request.query ?? {});
115
+ const only = typeof q.app === 'string' && q.app !== '' ? q.app : undefined;
116
+ return request.env(async (env) => ({ menus: this.menus(env, only) }));
117
+ });
118
+ app.get('/api/v1/actions/:id', async (request) => {
119
+ const id = String(request.params.id ?? '');
120
+ const owner = this.#loaded.actions.get(id);
121
+ if (owner === undefined)
122
+ throw new HttpProblem(404, 'unknown-action', 'Unknown action', `No action '${id}'.`);
123
+ return request.env(async (env) => {
124
+ if (!this.actionVisible(env, id))
125
+ throw new HttpProblem(403, 'access', 'Forbidden', `The action '${id}' is not available to this user.`, 'Ask an administrator for a role of its app.');
126
+ return { id, module: owner, spec: this.#module(owner).actions?.[id] ?? null };
127
+ });
128
+ });
129
+ }
130
+ }
131
+ //# sourceMappingURL=navigation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../../src/api/navigation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The OpenAPI 3.1 document of the installed module set (RFC 0002 §6.4), generated from the model metadata: one read
3
+ * schema, one create schema and one update schema per model, the generated paths, and RFC 9457 problems. Per-user
4
+ * restrictions (ACL, field groups, rules) are enforced at request time and visible in `/api/v1/meta/{model}`.
5
+ */
6
+ import type { KoshModule } from '@isahaq/kosh-modules-sdk';
7
+ import { type ApiField, type ApiModel } from './metadata.js';
8
+ type Schema = Record<string, unknown>;
9
+ /** The read schema of one field value. */
10
+ export declare function fieldSchema(field: ApiField): Schema;
11
+ /** The OpenAPI 3.1 document for these models. */
12
+ export declare function openApiDocument(models: Iterable<ApiModel>, info: {
13
+ readonly title: string;
14
+ readonly version: string;
15
+ }): Schema;
16
+ /**
17
+ * The OpenAPI document of a module set, without a database or a running server (what `kosh generate client` uses).
18
+ * @throws {ServerConfigError} for modules that cannot load together
19
+ */
20
+ export declare function openApiOf(modules: readonly KoshModule[]): Schema;
21
+ export {};
22
+ //# sourceMappingURL=openapi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/api/openapi.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAE3D,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAkC,MAAM,eAAe,CAAC;AAE7F,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAUtC,0CAA0C;AAC1C,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,MAAM,CAiDnD;AAsOD,iDAAiD;AACjD,wBAAgB,eAAe,CAC7B,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAC1B,IAAI,EAAE;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACzD,MAAM,CA6DR;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,CAGhE"}
@@ -0,0 +1,349 @@
1
+ import { buildKoshRegistry, KOSH_VERSION } from '../registry.js';
2
+ import { defaultFields, publicApiModels } from './metadata.js';
3
+ const PROBLEM = { $ref: '#/components/schemas/Problem' };
4
+ function nullable(schema, required) {
5
+ if (required)
6
+ return schema;
7
+ if (typeof schema['type'] === 'string')
8
+ return { ...schema, type: [schema['type'], 'null'] };
9
+ return { oneOf: [schema, { type: 'null' }] };
10
+ }
11
+ /** The read schema of one field value. */
12
+ export function fieldSchema(field) {
13
+ const base = (() => {
14
+ switch (field.kind) {
15
+ case 'char':
16
+ case 'text':
17
+ case 'html':
18
+ return { type: 'string' };
19
+ case 'boolean':
20
+ return { type: 'boolean' };
21
+ case 'integer':
22
+ case 'bigint':
23
+ return { type: 'integer' };
24
+ case 'float':
25
+ return { type: 'number' };
26
+ case 'decimal':
27
+ return {
28
+ type: 'string',
29
+ pattern: '^-?\\d+(\\.\\d+)?$',
30
+ description: 'Exact decimal as a string (never a float).',
31
+ };
32
+ case 'monetary':
33
+ return { $ref: '#/components/schemas/Money' };
34
+ case 'quantity':
35
+ return { $ref: '#/components/schemas/Quantity' };
36
+ case 'date':
37
+ return { type: 'string', format: 'date' };
38
+ case 'datetime':
39
+ return { type: 'string', format: 'date-time' };
40
+ case 'selection':
41
+ case 'state':
42
+ return field.options === null
43
+ ? { type: 'string' }
44
+ : { type: 'string', enum: field.options.map(([v]) => v) };
45
+ case 'many2one':
46
+ return { type: 'integer', description: `Id of a ${field.target} record.` };
47
+ case 'one2many':
48
+ case 'many2many':
49
+ return {
50
+ type: 'array',
51
+ items: { type: 'integer' },
52
+ description: `Ids of ${field.target} records.`,
53
+ };
54
+ case 'binary':
55
+ return { type: 'string', contentEncoding: 'base64' };
56
+ default:
57
+ return {};
58
+ }
59
+ })();
60
+ return { ...nullable(base, field.required || field.key === 'id'), title: field.label };
61
+ }
62
+ function writeSchema(field) {
63
+ if (field.kind === 'one2many' || field.kind === 'many2many')
64
+ return {
65
+ type: 'object',
66
+ title: field.label,
67
+ description: `Commands on ${field.target} records.`,
68
+ properties: {
69
+ create: { type: 'array', items: { type: 'object' } },
70
+ update: {
71
+ type: 'array',
72
+ items: {
73
+ type: 'object',
74
+ properties: { id: { type: 'integer' }, values: { type: 'object' } },
75
+ required: ['id', 'values'],
76
+ },
77
+ },
78
+ delete: { type: 'array', items: { type: 'integer' } },
79
+ link: { type: 'array', items: { type: 'integer' } },
80
+ unlink: { type: 'array', items: { type: 'integer' } },
81
+ set: { type: 'array', items: { type: 'integer' } },
82
+ clear: { type: 'boolean', const: true },
83
+ },
84
+ additionalProperties: false,
85
+ };
86
+ if (field.kind === 'monetary')
87
+ return nullable({
88
+ type: 'string',
89
+ pattern: '^-?\\d+(\\.\\d+)?$',
90
+ description: 'Amount in the record currency.',
91
+ }, field.required);
92
+ if (field.translate)
93
+ return nullable({ type: 'string', description: 'Text in the request language.' }, field.required);
94
+ return fieldSchema(field);
95
+ }
96
+ function modelSchemas(model) {
97
+ const readable = model.fields.filter((f) => f.kind !== 'binary');
98
+ const writable = model.fields.filter((f) => !f.readonly);
99
+ return {
100
+ [model.schemaName]: {
101
+ type: 'object',
102
+ title: model.label,
103
+ properties: Object.fromEntries(readable.map((f) => [f.key, fieldSchema(f)])),
104
+ required: defaultFields(model)
105
+ .filter((f) => f.required || f.key === 'id')
106
+ .map((f) => f.key),
107
+ },
108
+ [`${model.schemaName}Create`]: {
109
+ type: 'object',
110
+ properties: Object.fromEntries(writable.map((f) => [f.key, writeSchema(f)])),
111
+ required: writable.filter((f) => f.requiredOnCreate).map((f) => f.key),
112
+ additionalProperties: false,
113
+ },
114
+ [`${model.schemaName}Update`]: {
115
+ type: 'object',
116
+ properties: Object.fromEntries(writable.map((f) => [f.key, writeSchema(f)])),
117
+ additionalProperties: false,
118
+ },
119
+ };
120
+ }
121
+ function responses(ok, okStatus = '200') {
122
+ const problem = { content: { 'application/problem+json': { schema: PROBLEM } } };
123
+ return {
124
+ [okStatus]: { description: 'OK', content: { 'application/json': { schema: ok } } },
125
+ '400': { description: 'Invalid request', ...problem },
126
+ '401': { description: 'Not authenticated', ...problem },
127
+ '403': { description: 'Forbidden', ...problem },
128
+ '404': { description: 'Not found', ...problem },
129
+ '412': { description: 'Changed since read (If-Match)', ...problem },
130
+ '422': { description: 'Invalid values', ...problem },
131
+ '429': { description: 'Rate limited', ...problem },
132
+ };
133
+ }
134
+ const ID_PARAM = {
135
+ name: 'id',
136
+ in: 'path',
137
+ required: true,
138
+ schema: { type: 'integer', minimum: 1 },
139
+ };
140
+ const IDEMPOTENCY = {
141
+ name: 'Idempotency-Key',
142
+ in: 'header',
143
+ required: false,
144
+ schema: { type: 'string', maxLength: 128 },
145
+ description: 'Replays the first response for a retried request with the same key.',
146
+ };
147
+ function modelPaths(model) {
148
+ const s = model.schemaName;
149
+ const ref = { $ref: `#/components/schemas/${s}` };
150
+ const base = `/api/v1/models/${model.name}`;
151
+ const tag = [model.name];
152
+ const paths = {
153
+ [base]: {
154
+ get: {
155
+ operationId: `list${s}`,
156
+ tags: tag,
157
+ summary: `Search ${model.label} records`,
158
+ parameters: [
159
+ {
160
+ name: 'filter',
161
+ in: 'query',
162
+ schema: { type: 'string' },
163
+ description: 'A Shutro domain as JSON, e.g. {"name":{"ilike":"acme%"}}.',
164
+ },
165
+ {
166
+ name: 'order',
167
+ in: 'query',
168
+ schema: { type: 'string' },
169
+ description: "Comma-separated, e.g. 'name asc,id desc'.",
170
+ },
171
+ {
172
+ name: 'fields',
173
+ in: 'query',
174
+ schema: { type: 'string' },
175
+ description: 'Comma-separated field keys.',
176
+ },
177
+ {
178
+ name: 'limit',
179
+ in: 'query',
180
+ schema: { type: 'integer', minimum: 1, maximum: 1000, default: 80 },
181
+ },
182
+ { name: 'offset', in: 'query', schema: { type: 'integer', minimum: 0, default: 0 } },
183
+ {
184
+ name: 'count',
185
+ in: 'query',
186
+ schema: { type: 'boolean' },
187
+ description: 'Also return the total count.',
188
+ },
189
+ ],
190
+ responses: responses({
191
+ type: 'object',
192
+ properties: { items: { type: 'array', items: ref }, count: { type: 'integer' } },
193
+ required: ['items'],
194
+ }),
195
+ },
196
+ post: {
197
+ operationId: `create${s}`,
198
+ tags: tag,
199
+ summary: `Create ${model.label} records`,
200
+ parameters: [IDEMPOTENCY],
201
+ requestBody: {
202
+ required: true,
203
+ content: { 'application/json': { schema: { $ref: `#/components/schemas/${s}Create` } } },
204
+ },
205
+ responses: responses(ref, '201'),
206
+ },
207
+ },
208
+ [`${base}/{id}`]: {
209
+ get: { operationId: `get${s}`, tags: tag, parameters: [ID_PARAM], responses: responses(ref) },
210
+ patch: {
211
+ operationId: `update${s}`,
212
+ tags: tag,
213
+ parameters: [
214
+ ID_PARAM,
215
+ IDEMPOTENCY,
216
+ {
217
+ name: 'If-Match',
218
+ in: 'header',
219
+ schema: { type: 'string' },
220
+ description: 'The ETag read with the record: refuses the update (412) if it changed since.',
221
+ },
222
+ ],
223
+ requestBody: {
224
+ required: true,
225
+ content: { 'application/json': { schema: { $ref: `#/components/schemas/${s}Update` } } },
226
+ },
227
+ responses: responses(ref),
228
+ },
229
+ delete: {
230
+ operationId: `delete${s}`,
231
+ tags: tag,
232
+ parameters: [ID_PARAM, IDEMPOTENCY],
233
+ responses: responses({ type: 'object', properties: { deleted: { type: 'integer' } } }),
234
+ },
235
+ },
236
+ [`${base}/aggregate`]: {
237
+ post: {
238
+ operationId: `aggregate${s}`,
239
+ tags: tag,
240
+ requestBody: {
241
+ required: true,
242
+ content: {
243
+ 'application/json': { schema: { $ref: '#/components/schemas/AggregateQuery' } },
244
+ },
245
+ },
246
+ responses: responses({
247
+ type: 'object',
248
+ properties: { rows: { type: 'array', items: { type: 'object' } } },
249
+ }),
250
+ },
251
+ },
252
+ [`${base}/onchange`]: {
253
+ post: {
254
+ operationId: `onchange${s}`,
255
+ tags: tag,
256
+ requestBody: {
257
+ required: true,
258
+ content: {
259
+ 'application/json': { schema: { $ref: '#/components/schemas/OnchangeInput' } },
260
+ },
261
+ },
262
+ responses: responses({ $ref: '#/components/schemas/OnchangeResult' }),
263
+ },
264
+ },
265
+ };
266
+ for (const t of model.transitions)
267
+ paths[`${base}/{id}/transitions/${t}`] = {
268
+ post: {
269
+ operationId: `${t}${s}`,
270
+ tags: tag,
271
+ summary: `Transition: ${t}`,
272
+ parameters: [ID_PARAM, IDEMPOTENCY],
273
+ responses: responses(ref),
274
+ },
275
+ };
276
+ return paths;
277
+ }
278
+ /** The OpenAPI 3.1 document for these models. */
279
+ export function openApiDocument(models, info) {
280
+ const list = [...models];
281
+ return {
282
+ openapi: '3.1.0',
283
+ info,
284
+ paths: Object.assign({}, ...list.map(modelPaths)),
285
+ components: {
286
+ schemas: {
287
+ Problem: {
288
+ type: 'object',
289
+ properties: {
290
+ type: { type: 'string' },
291
+ title: { type: 'string' },
292
+ status: { type: 'integer' },
293
+ detail: { type: 'string' },
294
+ hint: { type: 'string' },
295
+ },
296
+ required: ['type', 'title', 'status', 'detail'],
297
+ },
298
+ Money: {
299
+ type: 'object',
300
+ properties: { amount: { type: 'string' }, currency: { type: 'string' } },
301
+ required: ['amount', 'currency'],
302
+ },
303
+ Quantity: {
304
+ type: 'object',
305
+ properties: { value: { type: 'string' }, uom: { type: 'string' } },
306
+ required: ['value', 'uom'],
307
+ },
308
+ AggregateQuery: {
309
+ type: 'object',
310
+ properties: {
311
+ where: { type: 'object' },
312
+ groupBy: { type: 'array', items: { type: 'string' } },
313
+ measures: { type: 'object' },
314
+ order: { type: 'array', items: { type: 'string' } },
315
+ limit: { type: 'integer' },
316
+ },
317
+ required: ['measures'],
318
+ },
319
+ OnchangeInput: {
320
+ type: 'object',
321
+ properties: {
322
+ id: { type: 'integer' },
323
+ values: { type: 'object' },
324
+ changed: { type: 'array', items: { type: 'string' } },
325
+ },
326
+ required: ['values', 'changed'],
327
+ },
328
+ OnchangeResult: {
329
+ type: 'object',
330
+ properties: {
331
+ values: { type: 'object' },
332
+ warnings: { type: 'array', items: { type: 'string' } },
333
+ children: { type: 'object' },
334
+ },
335
+ },
336
+ ...Object.assign({}, ...list.map(modelSchemas)),
337
+ },
338
+ },
339
+ };
340
+ }
341
+ /**
342
+ * The OpenAPI document of a module set, without a database or a running server (what `kosh generate client` uses).
343
+ * @throws {ServerConfigError} for modules that cannot load together
344
+ */
345
+ export function openApiOf(modules) {
346
+ const models = publicApiModels(buildKoshRegistry(modules));
347
+ return openApiDocument(models.values(), { title: 'Kosh API', version: KOSH_VERSION });
348
+ }
349
+ //# sourceMappingURL=openapi.js.map