@mh-gg/schema 0.1.1-alpha.20260613T085325975Z

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 (68) hide show
  1. package/package.json +39 -0
  2. package/schemas/app-composition.schema.json +684 -0
  3. package/schemas/example-app.schema.json +64 -0
  4. package/schemas/example-demo.schema.json +45 -0
  5. package/schemas/micro-plugin.schema.json +20 -0
  6. package/src/actions.cjs +31 -0
  7. package/src/app.d.ts +119 -0
  8. package/src/builder/app.cjs +157 -0
  9. package/src/builder/effects.cjs +116 -0
  10. package/src/builder/guards.cjs +35 -0
  11. package/src/builder/index.cjs +11 -0
  12. package/src/builder/matterhornApp/build.cjs +137 -0
  13. package/src/builder/matterhornApp/bundle.cjs +89 -0
  14. package/src/builder/matterhornApp/demo.cjs +161 -0
  15. package/src/builder/matterhornApp/demoAliases.cjs +50 -0
  16. package/src/builder/matterhornApp/descriptors.cjs +59 -0
  17. package/src/builder/matterhornApp/exports.cjs +95 -0
  18. package/src/builder/matterhornApp/frontend.cjs +80 -0
  19. package/src/builder/matterhornApp/plugins.cjs +95 -0
  20. package/src/builder/matterhornApp/shared.cjs +105 -0
  21. package/src/builder/matterhornApp.cjs +7 -0
  22. package/src/builder/model.cjs +172 -0
  23. package/src/builder/notifications.cjs +51 -0
  24. package/src/builder/refs.cjs +33 -0
  25. package/src/builder/schema.cjs +101 -0
  26. package/src/builder/streamKey.cjs +27 -0
  27. package/src/composition.cjs +157 -0
  28. package/src/configured.cjs +41 -0
  29. package/src/configured.d.ts +1 -0
  30. package/src/imports/loader.cjs +86 -0
  31. package/src/index.cjs +12 -0
  32. package/src/index.d.ts +168 -0
  33. package/src/json.cjs +70 -0
  34. package/src/jsonSchema/validator.cjs +131 -0
  35. package/src/microPlugin.cjs +112 -0
  36. package/src/model/collections.cjs +57 -0
  37. package/src/model/effects.cjs +149 -0
  38. package/src/model/expressions.cjs +85 -0
  39. package/src/model/guards.cjs +139 -0
  40. package/src/model/index.cjs +9 -0
  41. package/src/model/partitionOperands.cjs +89 -0
  42. package/src/model/partitionValidator.cjs +95 -0
  43. package/src/model/payload.cjs +93 -0
  44. package/src/model/plugin.cjs +146 -0
  45. package/src/notifications.cjs +41 -0
  46. package/src/notifications.d.ts +63 -0
  47. package/src/registry.cjs +63 -0
  48. package/src/runtime-exports.d.ts +65 -0
  49. package/src/streamKey.d.ts +15 -0
  50. package/src/types/actionTypes.cjs +164 -0
  51. package/src/types/coreActionPayloadSchemas.cjs +35 -0
  52. package/src/types/coreFeatures.cjs +29 -0
  53. package/src/types/entities.cjs +131 -0
  54. package/src/types/entityAliases.cjs +84 -0
  55. package/src/types/generator.cjs +145 -0
  56. package/src/types/index.cjs +5 -0
  57. package/src/types/interfaceBlock.cjs +41 -0
  58. package/src/types/pluginEntities.cjs +83 -0
  59. package/src/types/schema.cjs +148 -0
  60. package/src/types/standardPluginEntityTypes.cjs +135 -0
  61. package/test/content-lww-contract.test.cjs +104 -0
  62. package/test/partitionValidator.test.cjs +92 -0
  63. package/test/schema-array-effects.test.cjs +116 -0
  64. package/test/schema-builder-types.test.cjs +383 -0
  65. package/test/schema-composition.test.cjs +144 -0
  66. package/test/schema-configured-builders.test.cjs +286 -0
  67. package/test/schema-imports.test.cjs +66 -0
  68. package/test/schema-model-interpreter.test.cjs +273 -0
@@ -0,0 +1,64 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://matterhorn.gg/schemas/example-app.schema.json",
4
+ "title": "Matterhorn example app definition",
5
+ "type": "object",
6
+ "required": ["kind", "schemaVersion", "slug", "packageName", "appId", "pluginId", "version", "name", "composition"],
7
+ "properties": {
8
+ "$schema": { "type": "string" },
9
+ "$imports": { "type": ["array", "object"] },
10
+ "kind": { "const": "matterhorn.example-app.schema" },
11
+ "schemaVersion": { "const": 1 },
12
+ "slug": { "type": "string", "minLength": 1 },
13
+ "packageName": { "type": "string", "minLength": 1 },
14
+ "appId": { "type": "string", "minLength": 1 },
15
+ "pluginId": { "type": "string", "minLength": 1 },
16
+ "version": { "type": "string", "minLength": 1 },
17
+ "name": { "type": "string", "minLength": 1 },
18
+ "exportPrefix": { "type": "string", "minLength": 1 },
19
+ "constantPrefix": { "type": "string", "minLength": 1 },
20
+ "composition": { "type": "string", "minLength": 1 },
21
+ "demo": { "type": "string", "minLength": 1 },
22
+ "frontend": {
23
+ "oneOf": [
24
+ { "const": false },
25
+ {
26
+ "type": "object",
27
+ "properties": {
28
+ "port": { "type": "integer", "minimum": 1 },
29
+ "defaultPort": { "type": "integer", "minimum": 1 },
30
+ "bundlePort": { "type": "integer", "minimum": 1 },
31
+ "devEntry": { "type": "string" },
32
+ "builtEntry": { "type": "string" },
33
+ "backgroundColor": { "type": "string", "minLength": 1, "maxLength": 128 },
34
+ "icon": {
35
+ "oneOf": [
36
+ { "type": "string", "minLength": 1, "maxLength": 512 },
37
+ {
38
+ "type": "object",
39
+ "properties": {
40
+ "path": { "type": "string", "minLength": 1, "maxLength": 512 },
41
+ "svg": { "type": "string", "minLength": 1, "maxLength": 65536 },
42
+ "label": { "type": "string", "minLength": 1, "maxLength": 120 }
43
+ },
44
+ "additionalProperties": false,
45
+ "anyOf": [
46
+ { "required": ["path"] },
47
+ { "required": ["svg"] }
48
+ ]
49
+ }
50
+ ]
51
+ },
52
+ "label": { "type": "string" }
53
+ },
54
+ "additionalProperties": false
55
+ }
56
+ ]
57
+ },
58
+ "matterhornApp": { "type": "object" },
59
+ "exportAliases": { "type": "object", "additionalProperties": { "type": "string", "minLength": 1 } },
60
+ "example": { "type": "object" },
61
+ "typeNamespace": { "type": "string", "minLength": 1 }
62
+ },
63
+ "additionalProperties": false
64
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://matterhorn.gg/schemas/example-demo.schema.json",
4
+ "title": "Matterhorn example demo replay schema",
5
+ "type": "object",
6
+ "required": ["kind", "schemaVersion", "roomId", "operations"],
7
+ "properties": {
8
+ "$schema": { "type": "string" },
9
+ "$imports": { "type": ["array", "object"] },
10
+ "kind": { "const": "matterhorn.example-demo.schema" },
11
+ "schemaVersion": { "const": 1 },
12
+ "roomId": { "type": "string", "minLength": 1 },
13
+ "startTime": { "type": "number" },
14
+ "operations": {
15
+ "type": "array",
16
+ "items": {
17
+ "type": "object",
18
+ "required": ["id", "type", "actor", "payload"],
19
+ "properties": {
20
+ "id": { "type": "string", "minLength": 1 },
21
+ "pluginId": { "type": "string", "minLength": 1 },
22
+ "plugin": { "type": "string", "minLength": 1 },
23
+ "type": { "type": "string", "minLength": 1 },
24
+ "actor": {
25
+ "type": "object",
26
+ "required": ["memberId", "deviceId", "role"],
27
+ "properties": {
28
+ "memberId": { "type": "string", "minLength": 1 },
29
+ "deviceId": { "type": "string", "minLength": 1 },
30
+ "role": { "type": "string", "minLength": 1 },
31
+ "displayName": { "type": "string" }
32
+ },
33
+ "additionalProperties": true
34
+ },
35
+ "seq": { "type": "integer", "minimum": 1 },
36
+ "createdAt": { "type": "number" },
37
+ "payload": { "type": "object" }
38
+ },
39
+ "additionalProperties": false
40
+ }
41
+ },
42
+ "summary": { "type": "object" }
43
+ },
44
+ "additionalProperties": false
45
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://matterhorn.gg/schemas/micro-plugin.schema.json",
4
+ "title": "Matterhorn reusable micro-plugin schema",
5
+ "type": "object",
6
+ "required": ["kind", "schemaVersion", "key", "id", "version", "source"],
7
+ "properties": {
8
+ "$schema": { "type": "string" },
9
+ "kind": { "const": "matterhorn.micro-plugin.schema" },
10
+ "schemaVersion": { "const": 1 },
11
+ "key": { "type": "string", "minLength": 1 },
12
+ "id": { "type": "string", "minLength": 1 },
13
+ "version": { "type": "string", "minLength": 1 },
14
+ "source": { "type": "object" },
15
+ "plugin": { "type": "object" },
16
+ "packageName": { "type": "string" },
17
+ "exportName": { "type": "string" }
18
+ },
19
+ "additionalProperties": true
20
+ }
@@ -0,0 +1,31 @@
1
+ const { builtinMicroPluginRegistry } = require("./registry.cjs");
2
+ const { validateAppCompositionSchema } = require("./composition.cjs");
3
+
4
+ function pluginIdForAction(descriptor, schema, registry = builtinMicroPluginRegistry) {
5
+ if (descriptor.plugin === "primary" || descriptor.plugin === "$primary") return schema.primaryPlugin.id;
6
+ if (descriptor.plugin === "core" || descriptor.plugin === "matterhorn.core") return "matterhorn.core";
7
+ return registry.get(descriptor.plugin).schema.id;
8
+ }
9
+
10
+ function createActionDispatchersFromComposition(schema, options = {}) {
11
+ const registry = options.registry || builtinMicroPluginRegistry;
12
+ const composition = validateAppCompositionSchema(schema, { registry });
13
+ const dispatchers = {};
14
+ for (const descriptor of composition.actions) {
15
+ dispatchers[descriptor.name] = (client, input = {}) => {
16
+ const payload = {
17
+ ...(descriptor.payloadDefaults || {}),
18
+ ...(input || {})
19
+ };
20
+ return client.dispatch({ schemaAction: descriptor.name, pluginId: pluginIdForAction(descriptor, composition, registry), type: descriptor.type, payload });
21
+ };
22
+ }
23
+ return Object.freeze(dispatchers);
24
+ }
25
+
26
+ function actionDescriptorsForPlugin(schema, pluginSelection) {
27
+ const composition = validateAppCompositionSchema(schema);
28
+ return Object.freeze(composition.actions.filter((action) => action.plugin === pluginSelection || action.plugin === pluginSelection?.key || action.plugin === pluginSelection?.id));
29
+ }
30
+
31
+ module.exports = { actionDescriptorsForPlugin, createActionDispatchersFromComposition, pluginIdForAction };
package/src/app.d.ts ADDED
@@ -0,0 +1,119 @@
1
+ import type { Json, Model, QueryDef, ScopeDef } from "./index";
2
+ export type { NotificationsBlock } from "./notifications";
3
+
4
+ export type MatterhornFrontendCommand = { command: string; args?: string[]; env?: Record<string, string> };
5
+ export type MatterhornFrontendIcon = string | {
6
+ path?: string;
7
+ svg?: string;
8
+ label?: string;
9
+ };
10
+ export type MatterhornFrontendOptions = false | {
11
+ kind?: string;
12
+ root?: string;
13
+ label?: string;
14
+ port?: number;
15
+ defaultPort?: number;
16
+ bundlePort?: number;
17
+ devEntry?: string;
18
+ builtEntry?: string;
19
+ backgroundColor?: string;
20
+ icon?: MatterhornFrontendIcon;
21
+ mountPath?: string;
22
+ healthPath?: string;
23
+ dev?: MatterhornFrontendCommand;
24
+ build?: MatterhornFrontendCommand;
25
+ dist?: string;
26
+ bundle?: Record<string, unknown>;
27
+ locations?: object[];
28
+ };
29
+
30
+ export type MatterhornAppOptions = {
31
+ slug?: string;
32
+ packageName?: string;
33
+ packageRoot?: string;
34
+ exportPrefix?: string;
35
+ constantPrefix?: string;
36
+ typeNamespace?: string;
37
+ frontend?: MatterhornFrontendOptions;
38
+ matterhorn?: object;
39
+ matterhornApp?: object;
40
+ deployment?: object;
41
+ example?: object;
42
+ demo?: object;
43
+ exportAliases?: Record<string, string>;
44
+ publisher?: object;
45
+ trust?: object;
46
+ appCapabilities?: object;
47
+ hostCapabilities?: object;
48
+ entrypoints?: object;
49
+ recommendedFor?: object;
50
+ playerRoutes?: object[];
51
+ playerActions?: object;
52
+ optimisticReducers?: object;
53
+ navigation?: object;
54
+ deviceHints?: object;
55
+ hostPlugin?: object;
56
+ hostPlugins?: Array<object | { plugin: object; config?: Json }>;
57
+ hostPluginExport?: string;
58
+ hostPluginSource?: string;
59
+ hostPluginDependsOn?: string[];
60
+ hostPluginConflictsWith?: string[];
61
+ hostPackExport?: string;
62
+ playerPackId?: string;
63
+ playerPackExport?: string;
64
+ playerPluginId?: string;
65
+ playerPluginExport?: string;
66
+ createdAt?: string;
67
+ summary?: (state: unknown) => object;
68
+ registry?: unknown;
69
+ compositionSchema?: object;
70
+ };
71
+
72
+ export type AppSpec = MatterhornAppOptions & {
73
+ id: string;
74
+ name?: string;
75
+ namespace?: string;
76
+ version?: string;
77
+ pluginId?: string;
78
+ model: Model | object;
79
+ plugins?: Array<string | { key?: string; id?: string; config?: Json }>;
80
+ scopes?: ScopeDef[];
81
+ views?: Record<string, QueryDef | object> | object[];
82
+ queries?: Record<string, QueryDef | object>;
83
+ actions?: object;
84
+ routes?: Array<{ path: string; component: string; requires?: unknown[]; requiredPlugins?: string[] }>;
85
+ notifications?: NotificationsBlock;
86
+ };
87
+
88
+ export interface AppDef {
89
+ readonly id: string;
90
+ readonly name: string;
91
+ readonly version: string;
92
+ readonly namespace: string;
93
+ toJSON(): object;
94
+ toJSONFragments(options?: object): { composition: object; model: object; actions: object };
95
+ toTypes(options?: { namespace?: string }): string;
96
+ emit(options?: EmitOptions): EmitResult;
97
+ toMatterhornApp(options?: MatterhornAppOptions): object;
98
+ toMatterhornExports(options?: MatterhornAppOptions): Record<string, unknown>;
99
+ toMatterhornBundle(options?: MatterhornAppOptions): Record<string, unknown>;
100
+ emitMatterhornBundle(options?: MatterhornAppOptions & { outDir?: string; fileName?: string }): { bundlePath: string; bundle: Record<string, unknown> };
101
+ }
102
+
103
+ export type EmitOptions = {
104
+ outDir?: string;
105
+ modelFile?: string;
106
+ actionsFile?: string;
107
+ compositionFile?: string;
108
+ typesFile?: string;
109
+ namespace?: string;
110
+ schemaPath?: string | false;
111
+ };
112
+
113
+ export type EmitResult = {
114
+ modelPath: string;
115
+ actionsPath: string;
116
+ compositionPath: string;
117
+ typesPath: string;
118
+ composition: object;
119
+ };
@@ -0,0 +1,157 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { createAppCompositionSchema } = require('../composition.cjs');
4
+ const { actionNameFromOperation } = require('../microPlugin.cjs');
5
+ const { builtinMicroPluginRegistry } = require('../registry.cjs');
6
+ const { cloneJson } = require('../json.cjs');
7
+ const { generateAppTypeDeclaration, namespaceNameForApp } = require('../types/index.cjs');
8
+ const { cleanObject } = require('./schema.cjs');
9
+ const { modelToJson, queryToJson, SCOPE_DEF, MODEL_DEF } = require('./model.cjs');
10
+ const {
11
+ APP_MATTERHORN_META,
12
+ createMatterhornAppBundle,
13
+ createMatterhornAppDescriptor,
14
+ createMatterhornAppExports,
15
+ emitMatterhornAppBundle,
16
+ matterhornMetadataForSpec
17
+ } = require('./matterhornApp.cjs');
18
+
19
+ const APP_DEF = Symbol.for('matterhorn.schema.builder.app');
20
+
21
+ function pluginSelectionForComposition(plugin) {
22
+ if (typeof plugin === 'string') return plugin;
23
+ if (plugin?.key) return cleanObject({ key: plugin.key, config: plugin.config });
24
+ if (plugin?.id) return cleanObject({ id: plugin.id, config: plugin.config });
25
+ throw new Error('plugins must be registry keys, ids, or plugin refs');
26
+ }
27
+
28
+ function pluginKeyForAction(plugin, registry) {
29
+ if (typeof plugin === 'string') return registry.get(plugin).schema.key;
30
+ return registry.get(plugin.key || plugin.id).schema.key;
31
+ }
32
+
33
+ function defaultActionsForApp(modelJson, plugins = [], registry = builtinMicroPluginRegistry) {
34
+ const actions = {};
35
+ for (const type of Object.keys(modelJson.operations || {}).sort()) actions[actionNameFromOperation(type)] = { plugin: 'primary', type };
36
+ for (const plugin of plugins) {
37
+ const key = pluginKeyForAction(plugin, registry);
38
+ const record = registry.get(key);
39
+ for (const action of record.schema.actions || []) {
40
+ const name = actions[action.name] ? `${key}.${action.name}` : action.name;
41
+ actions[name] = cleanObject({ plugin: key, type: action.type, payloadDefaults: action.payloadDefaults, payloadSchema: action.payloadSchema, requiredRole: action.requiredRole });
42
+ }
43
+ }
44
+ return actions;
45
+ }
46
+
47
+ function viewDescriptors(views = {}) {
48
+ if (Array.isArray(views)) return cloneJson(views);
49
+ const out = [];
50
+ for (const [name, view] of Object.entries(views || {})) {
51
+ if (view?.plugin || view?.query) out.push({ name, ...cloneJson(view) });
52
+ else out.push({ name, plugin: 'primary', kind: 'query', query: name, ...queryToJson(view) });
53
+ }
54
+ return out;
55
+ }
56
+
57
+ function normalizeRoute(route) {
58
+ const out = { path: route.path, component: route.component };
59
+ const required = route.requiredPlugins || route.requires;
60
+ if (required) {
61
+ out.requiredPlugins = required.map((item) => {
62
+ if (typeof item === 'string') return item;
63
+ if (item?.[MODEL_DEF]) return undefined;
64
+ if (item?.id) return item.id;
65
+ return item;
66
+ }).filter(Boolean);
67
+ }
68
+ return cleanObject(out);
69
+ }
70
+
71
+ function actionsArrayToObject(actions = []) {
72
+ const out = {};
73
+ for (const action of actions) out[action.name] = cleanObject({ plugin: action.plugin, type: action.type, payloadDefaults: action.payloadDefaults, payloadSchema: action.payloadSchema, requiredRole: action.requiredRole, label: action.label });
74
+ return out;
75
+ }
76
+
77
+ function appActionsForSpec(modelJson, plugins, specActions, registry) {
78
+ const defaults = defaultActionsForApp(modelJson, plugins, registry);
79
+ if (!specActions) return defaults;
80
+ const configured = Array.isArray(specActions) ? actionsArrayToObject(specActions) : specActions;
81
+ return { ...defaults, ...configured };
82
+ }
83
+
84
+ function splitCompositionForFiles(composition, options = {}) {
85
+ const modelPath = options.modelPath || './model.json';
86
+ const actionsPath = options.actionsPath || './actions.json';
87
+ const schemaPath = options.schemaPath === false ? undefined : (options.schemaPath || 'matterhorn-sdk/schemas/app-composition.schema.json');
88
+ const model = cloneJson(composition.primaryPlugin.model || {});
89
+ const actions = actionsArrayToObject(composition.actions || []);
90
+ const split = cleanObject({
91
+ ...(schemaPath ? { $schema: schemaPath } : {}),
92
+ $imports: [{ path: modelPath, into: 'primaryPlugin.model' }, { path: actionsPath, into: 'actions' }],
93
+ ...cloneJson(composition)
94
+ });
95
+ delete split.primaryPlugin.model;
96
+ delete split.actions;
97
+ delete split.schemaHash;
98
+ return { composition: split, model, actions };
99
+ }
100
+
101
+ function defineApp(spec = {}) {
102
+ const registry = spec.registry || builtinMicroPluginRegistry;
103
+ const modelJson = modelToJson(spec.model);
104
+ const plugins = (spec.plugins || []).map(pluginSelectionForComposition);
105
+ const sharedScopes = {};
106
+ for (const scope of spec.scopes || []) {
107
+ if (scope?.[SCOPE_DEF]) sharedScopes[scope.key] = scope.toJSON();
108
+ else if (scope?.key) sharedScopes[scope.key] = cloneJson(scope);
109
+ }
110
+ const appSpec = {
111
+ app: { id: spec.id, version: spec.version || '1.0.0', name: spec.name || spec.id },
112
+ primaryPlugin: { id: spec.pluginId || `${spec.id}.plugin`, version: spec.version || '1.0.0', model: modelJson },
113
+ plugins,
114
+ ...(Object.keys(sharedScopes).length ? { sharedScopes } : {}),
115
+ views: viewDescriptors(spec.views || spec.queries || modelJson.views || {}),
116
+ actions: appActionsForSpec(modelJson, plugins, spec.actions, registry),
117
+ routes: (spec.routes || []).map(normalizeRoute),
118
+ ...(spec.notifications ? { notifications: spec.notifications } : {})
119
+ };
120
+ const composition = createAppCompositionSchema(appSpec, registry);
121
+ const matterhornMetadata = matterhornMetadataForSpec(spec, registry);
122
+ const app = {
123
+ [APP_DEF]: true,
124
+ [APP_MATTERHORN_META]: matterhornMetadata,
125
+ id: composition.app.id,
126
+ name: composition.app.name,
127
+ version: composition.app.version,
128
+ namespace: spec.namespace || namespaceNameForApp(composition),
129
+ toJSON() { return cloneJson(composition); },
130
+ toJSONFragments(options = {}) { return splitCompositionForFiles(composition, options); },
131
+ toTypes(options = {}) { return generateAppTypeDeclaration(composition, { ...options, namespace: options.namespace || app.namespace, registry: options.registry || registry }); },
132
+ emit(options = {}) { return emitAppArtifacts(app, options); },
133
+ toMatterhornApp(options = {}) { return createMatterhornAppDescriptor(app, options); },
134
+ toMatterhornExports(options = {}) { return createMatterhornAppExports(app, options); },
135
+ toMatterhornBundle(options = {}) { return createMatterhornAppBundle(app, options); },
136
+ emitMatterhornBundle(options = {}) { return emitMatterhornAppBundle(app, options); }
137
+ };
138
+ return Object.freeze(app);
139
+ }
140
+
141
+ function writeJson(filePath, value) {
142
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
143
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
144
+ }
145
+
146
+ function emitAppArtifacts(app, options = {}) {
147
+ if (!app?.[APP_DEF]) throw new Error('emitAppArtifacts requires a defineApp() result');
148
+ const outDir = path.resolve(options.outDir || '.');
149
+ const schemaFile = options.schemaFile || options.compositionFile || 'matterhorn.schema.json';
150
+ const typesFile = options.typesFile || 'matterhorn.types.d.ts';
151
+ writeJson(path.join(outDir, schemaFile), app.toJSON());
152
+ fs.mkdirSync(path.dirname(path.join(outDir, typesFile)), { recursive: true });
153
+ fs.writeFileSync(path.join(outDir, typesFile), app.toTypes({ namespace: options.namespace || app.namespace }));
154
+ return Object.freeze({ schemaPath: path.join(outDir, schemaFile), typesPath: path.join(outDir, typesFile), composition: app.toJSON() });
155
+ }
156
+
157
+ module.exports = { APP_DEF, defineApp, emitAppArtifacts, splitCompositionForFiles };
@@ -0,0 +1,116 @@
1
+ const { cloneJson, nonEmptyString } = require('../json.cjs');
2
+ const { cleanObject, ensurePlainObject } = require('./schema.cjs');
3
+
4
+ const COLLECTION_HANDLE = Symbol.for('matterhorn.schema.builder.collection');
5
+
6
+ const READ_CLASSES = Object.freeze(['window', 'head', 'full-fold']);
7
+ const INTEGRITY_CLASSES = Object.freeze(['signature', 'seq', 'checkpoint']);
8
+
9
+ function normalizeReadClass(value) {
10
+ if (!value) return 'full-fold';
11
+ const v = String(value);
12
+ return READ_CLASSES.includes(v) ? v : 'full-fold';
13
+ }
14
+
15
+ function normalizeIntegrityClass(value) {
16
+ if (!value) return 'checkpoint';
17
+ const v = String(value);
18
+ return INTEGRITY_CLASSES.includes(v) ? v : 'checkpoint';
19
+ }
20
+
21
+ function collectionHandle(name, sample, options = {}) {
22
+ const storage = options.storage || (Array.isArray(sample) ? 'array' : 'map');
23
+ const readClass = normalizeReadClass(options.readClass);
24
+ const integrityClass = normalizeIntegrityClass(options.integrityClass);
25
+ const handle = {
26
+ [COLLECTION_HANDLE]: true,
27
+ name: nonEmptyString(name, 'collection name'),
28
+ storage,
29
+ readClass,
30
+ integrityClass
31
+ };
32
+ return Object.freeze(handle);
33
+ }
34
+
35
+ function isCollection(value) {
36
+ return Boolean(value && typeof value === 'object' && value[COLLECTION_HANDLE]);
37
+ }
38
+
39
+ function normalizeCollection(collection, name = 'collection') {
40
+ if (isCollection(collection)) return collection;
41
+ if (typeof collection === 'string') return collectionHandle(collection, {}, { storage: 'map' });
42
+ throw new Error(`${name} must be a collection handle or collection name`);
43
+ }
44
+
45
+ function idRefParts(value, fallbackPrefix) {
46
+ if (typeof value !== 'string') return {};
47
+ const match = /^\$payload\.([A-Za-z0-9_-]+)$/.exec(value);
48
+ if (match) return { idField: match[1] };
49
+ const idPrefix = /^\$id:([A-Za-z0-9_-]+)$/.exec(value);
50
+ if (idPrefix && fallbackPrefix !== false) return { idPrefix: idPrefix[1] };
51
+ return { id: value };
52
+ }
53
+
54
+ function fieldNameFromPayloadRef(value) {
55
+ if (typeof value !== 'string') return undefined;
56
+ const match = /^\$payload\.([A-Za-z0-9_-]+)$/.exec(value);
57
+ return match ? match[1] : undefined;
58
+ }
59
+
60
+ function collectionEffectBase(kind, collection) {
61
+ const c = normalizeCollection(collection);
62
+ return { kind, collection: c.name, storage: c.storage, partition: c.name };
63
+ }
64
+
65
+ function normalizeFields(fields = {}) {
66
+ ensurePlainObject(fields, 'fields');
67
+ return cloneJson(fields);
68
+ }
69
+
70
+ const fx = Object.freeze({
71
+ noop() { return { kind: 'noop' }; },
72
+ create(collection, spec = {}) {
73
+ const base = collectionEffectBase('createRecord', collection);
74
+ const idParts = spec.id !== undefined ? idRefParts(spec.id) : (spec.idPrefix ? { idPrefix: spec.idPrefix } : {});
75
+ return cleanObject({ ...base, ...idParts, fields: normalizeFields(spec.fields || {}), activity: spec.activity, recordLabel: spec.recordLabel });
76
+ },
77
+ update(collection, spec = {}) {
78
+ return cleanObject({ ...collectionEffectBase('updateRecord', collection), ...idRefParts(spec.id, false), fields: normalizeFields(spec.set || spec.fields || {}), activity: spec.activity, recordLabel: spec.recordLabel });
79
+ },
80
+ mark(collection, spec = {}) {
81
+ return cleanObject({ ...collectionEffectBase('markRecord', collection), ...idRefParts(spec.id, false), fields: normalizeFields(spec.set || spec.fields || {}), activity: spec.activity, recordLabel: spec.recordLabel });
82
+ },
83
+ merge(spec = {}) {
84
+ return cleanObject({ kind: 'mergePath', path: nonEmptyString(spec.path, 'merge path'), fields: spec.fields === undefined ? '$payload' : cloneJson(spec.fields), deleteNullFields: spec.deleteNullFields, activity: spec.activity });
85
+ },
86
+ append(pathOrCollection, item = '$payload') {
87
+ const pathValue = isCollection(pathOrCollection) ? pathOrCollection.name : pathOrCollection;
88
+ return { kind: 'appendToArray', path: nonEmptyString(pathValue, 'append path'), item: cloneJson(item) };
89
+ },
90
+ toggleReaction(collection, spec = {}) {
91
+ const idField = fieldNameFromPayloadRef(spec.id);
92
+ const emojiField = fieldNameFromPayloadRef(spec.emoji);
93
+ return cleanObject({ ...collectionEffectBase('toggleReaction', collection), ...(idField ? { idField } : idRefParts(spec.id, false)), ...(emojiField ? { emojiField } : { emoji: spec.emoji }), activity: spec.activity, recordLabel: spec.recordLabel });
94
+ },
95
+ insertIntoArray(collection, spec = {}) {
96
+ return cleanObject({ ...collectionEffectBase('insertIdIntoRecordArray', collection), ...idRefParts(spec.matchId || spec.id, false), arrayField: nonEmptyString(spec.field || spec.arrayField, 'array field'), value: cloneJson(spec.value), position: spec.at || spec.position, activity: spec.activity, recordLabel: spec.recordLabel });
97
+ },
98
+ removeFromArray(collection, spec = {}) {
99
+ return cleanObject({ ...collectionEffectBase('removeIdFromRecordArray', collection), ...idRefParts(spec.matchId || spec.id, false), arrayField: nonEmptyString(spec.field || spec.arrayField, 'array field'), value: cloneJson(spec.value), activity: spec.activity, recordLabel: spec.recordLabel });
100
+ },
101
+ upsertActor(collection, spec = {}) {
102
+ return cleanObject({ ...collectionEffectBase('upsertActorRecord', collection), fields: normalizeFields(spec.fields || {}), activity: spec.activity, recordLabel: spec.recordLabel });
103
+ }
104
+ });
105
+
106
+ module.exports = {
107
+ COLLECTION_HANDLE,
108
+ collectionEffectBase,
109
+ collectionHandle,
110
+ fx,
111
+ idRefParts,
112
+ isCollection,
113
+ normalizeCollection,
114
+ normalizeReadClass,
115
+ normalizeIntegrityClass
116
+ };
@@ -0,0 +1,35 @@
1
+ const { cloneJson, nonEmptyString } = require('../json.cjs');
2
+ const { cleanObject } = require('./schema.cjs');
3
+ const { collectionEffectBase, idRefParts } = require('./effects.cjs');
4
+
5
+ function guardOperand(value) {
6
+ if (value && typeof value === 'object' && !Array.isArray(value) && typeof value.kind === 'string' && value.kind.startsWith('value.')) return cloneJson(value);
7
+ if (typeof value === 'string' && value.startsWith('$')) return { kind: 'value.expr', expr: value };
8
+ return { kind: 'value.literal', value: cloneJson(value) };
9
+ }
10
+
11
+ const guard = Object.freeze({
12
+ path(pathValue) { return { kind: 'value.path', path: nonEmptyString(pathValue, 'guard path') }; },
13
+ length(pathValue) { return { kind: 'value.length', path: nonEmptyString(pathValue, 'guard length path') }; },
14
+ value(value) { return { kind: 'value.literal', value: cloneJson(value) }; },
15
+ expr(expr) { return { kind: 'value.expr', expr: nonEmptyString(expr, 'guard expression') }; },
16
+ ownerOrRole(collection, spec = {}) {
17
+ return cleanObject({ ...collectionEffectBase('recordOwnerOrRole', collection), ...idRefParts(spec.id || spec.match, false), ownerField: spec.ownerField, roles: spec.roles || [], message: spec.message, recordLabel: spec.recordLabel });
18
+ },
19
+ flagClear(collection, spec = {}) {
20
+ return cleanObject({ ...collectionEffectBase('recordFlagClear', collection), ...idRefParts(spec.id || spec.match, false), flag: nonEmptyString(spec.flag, 'guard flag'), message: spec.message, recordLabel: spec.recordLabel });
21
+ },
22
+ eq(left, right, message) { return cleanObject({ kind: 'eq', left: guardOperand(left), right: guardOperand(right), message }); },
23
+ ne(left, right, message) { return cleanObject({ kind: 'ne', left: guardOperand(left), right: guardOperand(right), message }); },
24
+ lt(left, right, message) { return cleanObject({ kind: 'lt', left: guardOperand(left), right: guardOperand(right), message }); },
25
+ lte(left, right, message) { return cleanObject({ kind: 'lte', left: guardOperand(left), right: guardOperand(right), message }); },
26
+ gt(left, right, message) { return cleanObject({ kind: 'gt', left: guardOperand(left), right: guardOperand(right), message }); },
27
+ gte(left, right, message) { return cleanObject({ kind: 'gte', left: guardOperand(left), right: guardOperand(right), message }); },
28
+ exists(pathValue, message) { return cleanObject({ kind: 'exists', path: nonEmptyString(pathValue, 'guard path'), message }); },
29
+ and(...guards) { return { kind: 'and', guards: cloneJson(guards.flat()) }; },
30
+ or(...guards) { return { kind: 'or', guards: cloneJson(guards.flat()) }; },
31
+ not(item) { return { kind: 'not', guard: cloneJson(item) }; },
32
+ noop() { return { kind: 'noop' }; }
33
+ });
34
+
35
+ module.exports = { guard, guardOperand };
@@ -0,0 +1,11 @@
1
+ module.exports = {
2
+ ...require('./schema.cjs'),
3
+ ...require('./effects.cjs'),
4
+ ...require('./refs.cjs'),
5
+ ...require('./guards.cjs'),
6
+ ...require('./model.cjs'),
7
+ ...require('./app.cjs'),
8
+ ...require('./notifications.cjs'),
9
+ ...require('./matterhornApp.cjs'),
10
+ ...require('./matterhornApp/demoAliases.cjs')
11
+ };