@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,95 @@
1
+ const { hashCanonical } = require('@mh-gg/base');
2
+
3
+ function assertCanonicalOperationDescriptor(plugin, descriptor) {
4
+ if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor) || !descriptor.operations || typeof descriptor.operations !== 'object' || Array.isArray(descriptor.operations)) {
5
+ throw new Error(`${plugin.id} must expose canonical operationSchemaDescriptor.operations`);
6
+ }
7
+ return descriptor;
8
+ }
9
+
10
+ function pluginSchemaDescriptor(plugin, fallbackStateDescriptor, fallbackOperationDescriptor) {
11
+ return {
12
+ state: plugin.stateSchemaDescriptor || fallbackStateDescriptor || { plugin: plugin.id, version: plugin.version },
13
+ operations: assertCanonicalOperationDescriptor(plugin, plugin.operationSchemaDescriptor || fallbackOperationDescriptor)
14
+ };
15
+ }
16
+
17
+ function collectHostPluginEntries(items) {
18
+ const out = [];
19
+ function collect(item) {
20
+ if (!item) return;
21
+ if (Array.isArray(item)) {
22
+ for (const child of item) collect(child);
23
+ return;
24
+ }
25
+ if (item.kind === 'matterhorn.example-plugin-suite' && Array.isArray(item.entries)) {
26
+ for (const child of item.entries) collect(child);
27
+ return;
28
+ }
29
+ if (Array.isArray(item.entries)) {
30
+ for (const child of item.entries) collect(child);
31
+ return;
32
+ }
33
+ out.push(item);
34
+ }
35
+ collect(items);
36
+ return out;
37
+ }
38
+
39
+ function normalizeHostPluginEntries(config, version) {
40
+ const entries = [];
41
+ if (config.hostPlugin) {
42
+ entries.push({
43
+ plugin: config.hostPlugin,
44
+ packageName: config.packageName,
45
+ exportName: config.hostPluginExport || 'hostPlugin',
46
+ source: config.hostPluginSource,
47
+ dependsOn: config.hostPluginDependsOn || [],
48
+ conflictsWith: config.hostPluginConflictsWith || []
49
+ });
50
+ }
51
+ for (const entry of collectHostPluginEntries(config.hostPlugins)) {
52
+ const normalized = entry.plugin ? { ...entry } : { plugin: entry };
53
+ if (!normalized.plugin) throw new Error('hostPlugins entries must include plugin');
54
+ const plugin = normalized.config === undefined ? normalized.plugin : { ...normalized.plugin, config: JSON.parse(JSON.stringify(normalized.config)) };
55
+ entries.push({
56
+ plugin,
57
+ packageName: normalized.packageName || config.packageName,
58
+ exportName: normalized.exportName || normalized.hostPluginExport || normalized.plugin.exportName || 'hostPlugin',
59
+ source: normalized.source,
60
+ dependsOn: normalized.dependsOn || normalized.plugin.dependsOn || [],
61
+ conflictsWith: normalized.conflictsWith || normalized.plugin.conflictsWith || [],
62
+ stateSchemaDescriptor: normalized.stateSchemaDescriptor,
63
+ operationSchemaDescriptor: normalized.operationSchemaDescriptor
64
+ });
65
+ }
66
+ if (entries.length === 0) throw new Error('At least one host plugin is required');
67
+ const seen = new Set();
68
+ for (const entry of entries) {
69
+ if (seen.has(entry.plugin.id)) throw new Error(`Duplicate host plugin ${entry.plugin.id}`);
70
+ seen.add(entry.plugin.id);
71
+ const descriptors = pluginSchemaDescriptor(entry.plugin, entry.stateSchemaDescriptor, entry.operationSchemaDescriptor);
72
+ entry.stateSchemaDescriptor = descriptors.state;
73
+ entry.operationSchemaDescriptor = descriptors.operations;
74
+ entry.plugin.stateSchemaHash = hashCanonical(entry.stateSchemaDescriptor);
75
+ entry.plugin.operationSchemaHash = hashCanonical(entry.operationSchemaDescriptor);
76
+ entry.version = entry.plugin.version || version;
77
+ }
78
+ return entries;
79
+ }
80
+
81
+ function primaryHostPluginForBuilt(built) {
82
+ const primaryId = built.config.hostPlugin?.id || built.compositionSchema.primaryPlugin.id;
83
+ return built.hostPlugins.find((plugin) => plugin.id === primaryId) || built.hostPlugins[0];
84
+ }
85
+
86
+ function primaryHostPluginDescriptorForBuilt(built) {
87
+ const primaryId = primaryHostPluginForBuilt(built).id;
88
+ return built.hostPluginDescriptors.find((descriptor) => descriptor.id === primaryId) || built.hostPluginDescriptors[0];
89
+ }
90
+
91
+ module.exports = {
92
+ normalizeHostPluginEntries,
93
+ primaryHostPluginDescriptorForBuilt,
94
+ primaryHostPluginForBuilt
95
+ };
@@ -0,0 +1,105 @@
1
+ const { cloneJson } = require('../../json.cjs');
2
+ const { namespaceNameForApp } = require('../../types/index.cjs');
3
+
4
+ const APP_MATTERHORN_META = Symbol.for('matterhorn.schema.builder.app.matterhorn');
5
+
6
+ const DEFAULT_PUBLISHER = Object.freeze({
7
+ id: 'com.matterhorn.schema',
8
+ name: 'Matterhorn Schema Builder',
9
+ publicKey: 'rk_pub_schema_builder'
10
+ });
11
+
12
+ const DEFAULT_TRUST = Object.freeze({
13
+ signatures: Object.freeze([{ publicKey: DEFAULT_PUBLISHER.publicKey, signature: 'sig_schema_builder' }])
14
+ });
15
+
16
+ function maybeClone(value) {
17
+ return value === undefined ? undefined : cloneJson(value);
18
+ }
19
+
20
+ function pascalCase(value) {
21
+ return String(value).replace(/(^|[^a-zA-Z0-9]+)([a-zA-Z0-9])/g, (_match, _sep, char) => char.toUpperCase()).replace(/[^a-zA-Z0-9]/g, '');
22
+ }
23
+
24
+ function slugFromAppId(appId) {
25
+ return String(appId).split('.').filter(Boolean).pop() || 'matterhorn-app';
26
+ }
27
+
28
+ function optionValue(options, key, fallback) {
29
+ return Object.prototype.hasOwnProperty.call(options, key) ? options[key] : fallback;
30
+ }
31
+
32
+ function matterhornMetadataForSpec(spec = {}, registry) {
33
+ return Object.freeze({
34
+ registry,
35
+ slug: spec.slug,
36
+ packageName: spec.packageName,
37
+ packageRoot: spec.packageRoot,
38
+ exportPrefix: spec.exportPrefix,
39
+ constantPrefix: spec.constantPrefix,
40
+ typeNamespace: spec.typeNamespace,
41
+ frontend: maybeClone(spec.frontend),
42
+ matterhorn: maybeClone(spec.matterhorn),
43
+ matterhornApp: maybeClone(spec.matterhornApp),
44
+ deployment: maybeClone(spec.deployment),
45
+ example: maybeClone(spec.example),
46
+ demo: maybeClone(spec.demo),
47
+ exportAliases: maybeClone(spec.exportAliases),
48
+ publisher: maybeClone(spec.publisher),
49
+ trust: maybeClone(spec.trust),
50
+ createdAt: spec.createdAt,
51
+ summary: spec.summary,
52
+ appCapabilities: maybeClone(spec.appCapabilities),
53
+ hostCapabilities: maybeClone(spec.hostCapabilities),
54
+ entrypoints: maybeClone(spec.entrypoints),
55
+ recommendedFor: maybeClone(spec.recommendedFor),
56
+ routes: maybeClone(spec.playerRoutes),
57
+ actions: spec.playerActions,
58
+ optimisticReducers: spec.optimisticReducers,
59
+ navigation: maybeClone(spec.navigation),
60
+ deviceHints: maybeClone(spec.deviceHints),
61
+ hostPlugin: spec.hostPlugin,
62
+ hostPlugins: spec.hostPlugins,
63
+ hostPluginExport: spec.hostPluginExport,
64
+ hostPluginSource: spec.hostPluginSource,
65
+ hostPluginDependsOn: maybeClone(spec.hostPluginDependsOn || spec.dependsOn),
66
+ hostPluginConflictsWith: maybeClone(spec.hostPluginConflictsWith),
67
+ hostPackExport: spec.hostPackExport,
68
+ playerPackId: spec.playerPackId,
69
+ playerPackExport: spec.playerPackExport,
70
+ playerPluginId: spec.playerPluginId,
71
+ playerPluginExport: spec.playerPluginExport
72
+ });
73
+ }
74
+
75
+ function mergedOptions(app, options = {}) {
76
+ const meta = app?.[APP_MATTERHORN_META] || {};
77
+ const merged = { ...meta, ...options };
78
+ for (const key of ['frontend', 'matterhorn', 'matterhornApp', 'deployment', 'example', 'demo', 'exportAliases', 'publisher', 'trust', 'appCapabilities', 'hostCapabilities']) {
79
+ merged[key] = optionValue(options, key, meta[key]);
80
+ }
81
+ if (Object.prototype.hasOwnProperty.call(options, 'playerRoutes')) merged.routes = maybeClone(options.playerRoutes);
82
+ if (Object.prototype.hasOwnProperty.call(options, 'playerActions')) merged.actions = options.playerActions;
83
+ if (!Object.prototype.hasOwnProperty.call(options, 'hostPluginDependsOn') && Object.prototype.hasOwnProperty.call(options, 'dependsOn')) {
84
+ merged.hostPluginDependsOn = maybeClone(options.dependsOn);
85
+ }
86
+ merged.registry = options.registry || meta.registry;
87
+ return merged;
88
+ }
89
+
90
+ function typeNamespaceForBuilt(built) {
91
+ const config = built.config;
92
+ return pascalCase(config.typeNamespace || config.exportPrefix || config.slug || namespaceNameForApp(built.compositionSchema));
93
+ }
94
+
95
+ module.exports = {
96
+ APP_MATTERHORN_META,
97
+ DEFAULT_PUBLISHER,
98
+ DEFAULT_TRUST,
99
+ maybeClone,
100
+ mergedOptions,
101
+ pascalCase,
102
+ matterhornMetadataForSpec,
103
+ slugFromAppId,
104
+ typeNamespaceForBuilt
105
+ };
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ ...require('./matterhornApp/shared.cjs'),
3
+ ...require('./matterhornApp/build.cjs'),
4
+ ...require('./matterhornApp/bundle.cjs'),
5
+ ...require('./matterhornApp/demo.cjs'),
6
+ ...require('./matterhornApp/exports.cjs')
7
+ };
@@ -0,0 +1,172 @@
1
+ const { cloneJson, freezeJson, nonEmptyString } = require('../json.cjs');
2
+ const { cleanObject, normalizePayloadShape, own } = require('./schema.cjs');
3
+ const { collectionHandle, isCollection, normalizeCollection, fx } = require('./effects.cjs');
4
+ const { normalizeReadClass, normalizeIntegrityClass, streamKey } = require('./streamKey.cjs');
5
+ const { guard } = require('./guards.cjs');
6
+ const { refForPayload, ref } = require('./refs.cjs');
7
+
8
+ const QUERY_DEF = Symbol.for('matterhorn.schema.builder.query');
9
+ const MODEL_DEF = Symbol.for('matterhorn.schema.builder.model');
10
+ const SCOPE_DEF = Symbol.for('matterhorn.schema.builder.scope');
11
+
12
+ function operationDefinition(spec = {}, build) {
13
+ const payload = normalizePayloadShape(spec.payload || {});
14
+ const body = typeof build === 'function' ? build({ ref: refForPayload(payload), fx, guard }) : (build || {});
15
+ const definition = cleanObject({
16
+ authorize: spec.authorize || {},
17
+ payload,
18
+ guards: body.guards || body.guard ? (body.guards || [body.guard]) : undefined,
19
+ effects: body.effects || []
20
+ });
21
+ if (!Array.isArray(definition.effects)) throw new Error('operation effects must be an array');
22
+ if (definition.guards && !Array.isArray(definition.guards)) throw new Error('operation guards must be an array');
23
+ return freezeJson(definition);
24
+ }
25
+
26
+ function op(spec, build) {
27
+ return operationDefinition(spec, build);
28
+ }
29
+
30
+ function roleDefinitionsFromSpec(roles = {}) {
31
+ const out = {};
32
+ for (const [id, role] of Object.entries(roles || {})) {
33
+ out[id] = cleanObject({
34
+ id,
35
+ name: role.name || id,
36
+ description: role.description,
37
+ color: role.color,
38
+ rank: Number.isFinite(Number(role.rank)) ? Number(role.rank) : 0,
39
+ systemRole: role.system ?? role.systemRole ?? true,
40
+ archivedAt: role.archivedAt === undefined ? null : role.archivedAt
41
+ });
42
+ }
43
+ return out;
44
+ }
45
+
46
+ function initialStateFromModelSpec(spec = {}) {
47
+ const state = spec.state && own(spec.state, 'initial') ? spec.state.initial : (spec.state || {});
48
+ const initial = cloneJson(state || {});
49
+ if (spec.roles && !initial.roleDefinitions) initial.roleDefinitions = roleDefinitionsFromSpec(spec.roles);
50
+ if (!initial.activity) initial.activity = [];
51
+ return initial;
52
+ }
53
+
54
+ function shapesFromCollections(collections = {}) {
55
+ const shapes = {};
56
+ for (const [name, handle] of Object.entries(collections)) {
57
+ shapes[name] = cleanObject({
58
+ readClass: handle.readClass,
59
+ integrityClass: handle.integrityClass,
60
+ storage: handle.storage,
61
+ partition: name
62
+ });
63
+ }
64
+ return shapes;
65
+ }
66
+
67
+ function makeModel(spec = {}, operations = {}) {
68
+ const initial = initialStateFromModelSpec(spec);
69
+ const collections = {};
70
+ for (const [name, sample] of Object.entries(initial)) {
71
+ const options = spec.shapes?.[name] || {};
72
+ collections[name] = collectionHandle(name, sample, options);
73
+ }
74
+ const shapes = shapesFromCollections(collections);
75
+ const model = {
76
+ [MODEL_DEF]: true,
77
+ roles: freezeJson(spec.roles || {}),
78
+ collections: Object.freeze(collections),
79
+ withOperations(nextOperations = {}) { return makeModel(spec, nextOperations); },
80
+ operation(name, definition) { return makeModel(spec, { ...operations, [name]: definition }); },
81
+ toJSON() {
82
+ return freezeJson({
83
+ kind: 'matterhorn.primary-model.schema',
84
+ schemaVersion: 1,
85
+ state: cleanObject({ initial, ...(spec.state?.roomIdPath ? { roomIdPath: spec.state.roomIdPath } : {}) }),
86
+ operations: cloneJson(operations),
87
+ views: cloneJson(spec.views || {}),
88
+ shapes: cloneJson(shapes),
89
+ ...(spec.publicView ? { publicView: cloneJson(spec.publicView) } : {}),
90
+ ...(spec.capabilities ? { capabilities: cloneJson(spec.capabilities) } : {})
91
+ });
92
+ }
93
+ };
94
+ return Object.freeze(model);
95
+ }
96
+
97
+ function defineModel(spec = {}) {
98
+ return makeModel(spec);
99
+ }
100
+
101
+ const q = Object.freeze({
102
+ state() { return Object.freeze({ [QUERY_DEF]: true, kind: 'state', toJSON() { return { kind: 'state' }; } }); },
103
+ collection(collection, options = {}) {
104
+ const c = normalizeCollection(collection);
105
+ const query = { [QUERY_DEF]: true, kind: 'collection', collection: c.name, path: options.path, includeArchived: options.includeArchived };
106
+ query.toJSON = () => cleanObject({ kind: 'collection', collection: c.name, path: options.path, includeArchived: options.includeArchived });
107
+ return Object.freeze(query);
108
+ },
109
+ count(collection, options = {}) {
110
+ const c = normalizeCollection(collection);
111
+ const query = { [QUERY_DEF]: true, kind: 'count', collection: c.name, path: options.path };
112
+ query.toJSON = () => cleanObject({ kind: 'count', collection: c.name, path: options.path });
113
+ return Object.freeze(query);
114
+ }
115
+ });
116
+
117
+ function queryToJson(value) {
118
+ if (value?.[QUERY_DEF] && typeof value.toJSON === 'function') return value.toJSON();
119
+ if (value && typeof value === 'object' && !Array.isArray(value)) return cloneJson(value);
120
+ throw new Error('view must be a query definition object');
121
+ }
122
+
123
+ function defineScope(spec = {}) {
124
+ const key = spec.key || spec.name || spec.type;
125
+ const scope = {
126
+ [SCOPE_DEF]: true,
127
+ key: nonEmptyString(key, 'scope key'),
128
+ scopeType: nonEmptyString(spec.type || spec.scopeType, 'scope type'),
129
+ scopeId: nonEmptyString(spec.id || spec.scopeId, 'scope id'),
130
+ identity: spec.identity,
131
+ participants: spec.participants,
132
+ collections: (spec.collections || []).map((item) => isCollection(item) ? item.name : String(item)),
133
+ description: spec.description,
134
+ readClass: normalizeReadClass(spec.readClass),
135
+ integrityClass: normalizeIntegrityClass(spec.integrityClass),
136
+ view: { action: 'view', scopeType: spec.type || spec.scopeType, scopeId: spec.id || spec.scopeId },
137
+ edit: { action: 'edit', scopeType: spec.type || spec.scopeType, scopeId: spec.id || spec.scopeId }
138
+ };
139
+ scope.toJSON = () => cleanObject({
140
+ scopeType: scope.scopeType,
141
+ scopeId: scope.scopeId,
142
+ identity: scope.identity,
143
+ participants: scope.participants,
144
+ collections: scope.collections,
145
+ description: scope.description,
146
+ readClass: scope.readClass,
147
+ integrityClass: scope.integrityClass
148
+ });
149
+ return Object.freeze(scope);
150
+ }
151
+
152
+ function modelToJson(model) {
153
+ if (model?.[MODEL_DEF] && typeof model.toJSON === 'function') return model.toJSON();
154
+ if (model && typeof model === 'object') return cloneJson(model);
155
+ throw new Error('defineApp requires a model');
156
+ }
157
+
158
+ module.exports = {
159
+ MODEL_DEF,
160
+ QUERY_DEF,
161
+ SCOPE_DEF,
162
+ defineModel,
163
+ defineScope,
164
+ modelToJson,
165
+ normalizeReadClass,
166
+ normalizeIntegrityClass,
167
+ op,
168
+ q,
169
+ queryToJson,
170
+ ref,
171
+ streamKey
172
+ };
@@ -0,0 +1,51 @@
1
+ const { cloneJson } = require("../json.cjs");
2
+
3
+ function defineNotification(spec = {}) {
4
+ if (!spec.on?.action) throw new Error("defineNotification requires on.action");
5
+ if (!spec.audience?.userIds) throw new Error("defineNotification requires audience.userIds");
6
+ if (!spec.scope) throw new Error("defineNotification requires scope");
7
+ if (!spec.presentation) throw new Error("defineNotification requires presentation");
8
+ if (!spec.read) throw new Error("defineNotification requires read");
9
+
10
+ return cleanObject({
11
+ on: cleanObject({
12
+ action: spec.on.action,
13
+ ...(spec.on.plugin ? { plugin: spec.on.plugin } : {}),
14
+ ...(spec.on.type ? { type: spec.on.type } : {})
15
+ }),
16
+ kind: spec.kind || spec.on.action,
17
+ ...(spec.privacy ? { privacy: spec.privacy } : {}),
18
+ audience: cleanObject({
19
+ userIds: spec.audience.userIds,
20
+ ...(spec.audience.excludeActor ? { excludeActor: true } : {})
21
+ }),
22
+ scope: cloneJson(spec.scope),
23
+ ...(spec.entity ? { entity: cloneJson(spec.entity) } : {}),
24
+ presentation: cloneJson(spec.presentation),
25
+ ...(spec.link ? { link: cloneJson(spec.link) } : {}),
26
+ read: cloneJson(spec.read),
27
+ ...(spec.delivery ? { delivery: cloneJson(spec.delivery) } : {})
28
+ });
29
+ }
30
+
31
+ function defineNotificationsBlock(spec = {}) {
32
+ const definitions = {};
33
+ for (const [name, def] of Object.entries(spec.definitions || {})) {
34
+ definitions[name] = typeof def === "function" ? def() : def;
35
+ }
36
+ return {
37
+ schemaVersion: spec.schemaVersion || 1,
38
+ definitions
39
+ };
40
+ }
41
+
42
+ function cleanObject(obj) {
43
+ if (!obj || typeof obj !== "object") return obj;
44
+ const out = {};
45
+ for (const [k, v] of Object.entries(obj)) {
46
+ if (v !== undefined) out[k] = v;
47
+ }
48
+ return out;
49
+ }
50
+
51
+ module.exports = { defineNotification, defineNotificationsBlock };
@@ -0,0 +1,33 @@
1
+ const { cloneJson, nonEmptyString } = require('../json.cjs');
2
+
3
+ function knownPayloadKeys(payloadSchema) {
4
+ return new Set([...Object.keys(payloadSchema.required || {}), ...Object.keys(payloadSchema.optional || {})]);
5
+ }
6
+
7
+ function refForPayload(payloadSchema = {}) {
8
+ const keys = knownPayloadKeys(payloadSchema);
9
+ function assertPayloadKey(key) {
10
+ if (keys.size > 0 && !keys.has(key)) throw new Error(`Unknown payload key ${key}`);
11
+ return key;
12
+ }
13
+ return Object.freeze({
14
+ payload(key, fallback) {
15
+ const expr = `$payload.${assertPayloadKey(String(key))}`;
16
+ if (arguments.length < 2) return expr;
17
+ return { $expr: expr, fallback: cloneJson(fallback) };
18
+ },
19
+ actor(field = 'memberId') { return `$actor.${String(field)}`; },
20
+ app(field = 'id') { return `$app.${String(field)}`; },
21
+ room(field = 'id') { return `$room.${String(field)}`; },
22
+ profile(field = 'id') { return `$profile.${String(field)}`; },
23
+ now() { return '$createdAt'; },
24
+ operation(field = 'id') { return `$operation.${String(field)}`; },
25
+ newId(prefix) { return `$id:${nonEmptyString(prefix, 'id prefix')}`; },
26
+ literal(value) { return { $literal: cloneJson(value) }; },
27
+ expr(value) { return { $expr: nonEmptyString(value, 'expression') }; }
28
+ });
29
+ }
30
+
31
+ const ref = refForPayload({});
32
+
33
+ module.exports = { knownPayloadKeys, ref, refForPayload };
@@ -0,0 +1,101 @@
1
+ const { cloneJson, freezeJson } = require('../json.cjs');
2
+
3
+ const SCHEMA_BUILDER = Symbol.for('matterhorn.schema.builder.schema');
4
+
5
+ function own(value, key) {
6
+ return Object.prototype.hasOwnProperty.call(value, key);
7
+ }
8
+
9
+ function cleanObject(value) {
10
+ const out = {};
11
+ for (const [key, entry] of Object.entries(value || {})) if (entry !== undefined) out[key] = entry;
12
+ return out;
13
+ }
14
+
15
+ function ensurePlainObject(value, name) {
16
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${name} must be an object`);
17
+ return value;
18
+ }
19
+
20
+ function makeSchema(spec, options = {}) {
21
+ const normalized = freezeJson(spec || { type: 'any' });
22
+ const state = Object.freeze({ optional: options.optional === true });
23
+ const api = {
24
+ [SCHEMA_BUILDER]: true,
25
+ optional() { return makeSchema(normalized, { ...state, optional: true }); },
26
+ nullable() { return makeSchema({ ...normalized, nullable: true }, state); },
27
+ default(value) { return makeSchema({ ...normalized, default: cloneJson(value) }, state); },
28
+ clearable() { return makeSchema({ ...normalized, clearable: true, nullable: true }, state); },
29
+ toJSON() { return cloneJson(normalized); }
30
+ };
31
+ Object.defineProperty(api, '__matterhornSchema', { value: state, enumerable: false });
32
+ return Object.freeze(api);
33
+ }
34
+
35
+ function isSchemaBuilder(value) {
36
+ return Boolean(value && typeof value === 'object' && value[SCHEMA_BUILDER]);
37
+ }
38
+
39
+ function schemaOptional(value) {
40
+ return Boolean(value?.__matterhornSchema?.optional);
41
+ }
42
+
43
+ function schemaJson(value, name = 'schema') {
44
+ if (isSchemaBuilder(value)) return value.toJSON();
45
+ if (typeof value === 'string') return { type: value };
46
+ if (value && typeof value === 'object' && !Array.isArray(value)) return cloneJson(value);
47
+ if (value === undefined) return { type: 'any' };
48
+ throw new Error(`${name} must be a Matterhorn payload schema`);
49
+ }
50
+
51
+ function payloadShapeFromSchemas(shape = {}, options = {}) {
52
+ ensurePlainObject(shape, 'payload shape');
53
+ const required = {};
54
+ const optional = {};
55
+ for (const [key, schema] of Object.entries(shape)) {
56
+ const target = schemaOptional(schema) ? optional : required;
57
+ target[key] = schemaJson(schema, `payload.${key}`);
58
+ }
59
+ return cleanObject({
60
+ ...(Object.keys(required).length ? { required } : {}),
61
+ ...(Object.keys(optional).length ? { optional } : {}),
62
+ ...(options.additional === undefined ? {} : { additional: options.additional })
63
+ });
64
+ }
65
+
66
+ function normalizePayloadShape(payload = {}) {
67
+ if (payload.required || payload.optional || payload.additional !== undefined) return cloneJson(payload);
68
+ return payloadShapeFromSchemas(payload);
69
+ }
70
+
71
+ function structuredObjectSchema(shape = {}, options = {}) {
72
+ return makeSchema({ type: 'object', ...payloadShapeFromSchemas(shape, { additional: options.additional ?? false }) });
73
+ }
74
+
75
+ const p = Object.freeze({
76
+ any() { return makeSchema({ type: 'any' }); },
77
+ string(opts = {}) { return makeSchema(cleanObject({ type: 'string', ...opts })); },
78
+ number(opts = {}) { return makeSchema(cleanObject({ type: 'number', ...opts })); },
79
+ boolean() { return makeSchema({ type: 'boolean' }); },
80
+ enum(values, opts = {}) {
81
+ if (!Array.isArray(values) || values.length === 0) throw new Error('p.enum requires a non-empty values array');
82
+ return makeSchema(cleanObject({ type: 'enum', values: values.map(String), ...opts }));
83
+ },
84
+ array(items, opts = {}) { return makeSchema(cleanObject({ type: 'array', items: items ? schemaJson(items, 'array item schema') : undefined, ...opts })); },
85
+ object(shape = {}, opts = {}) { return structuredObjectSchema(shape, opts); },
86
+ record(values = p.any(), opts = {}) { return makeSchema(cleanObject({ type: 'record', values: schemaJson(values, 'record value schema'), ...opts })); },
87
+ literal(value) { return makeSchema({ type: 'literal', value: cloneJson(value) }); }
88
+ });
89
+
90
+ module.exports = {
91
+ SCHEMA_BUILDER,
92
+ cleanObject,
93
+ ensurePlainObject,
94
+ isSchemaBuilder,
95
+ normalizePayloadShape,
96
+ own,
97
+ p,
98
+ payloadShapeFromSchemas,
99
+ schemaJson,
100
+ schemaOptional
101
+ };
@@ -0,0 +1,27 @@
1
+ const { nonEmptyString } = require('../json.cjs');
2
+
3
+ const READ_CLASSES = Object.freeze(new Set(['window', 'head', 'full-fold']));
4
+ const INTEGRITY_CLASSES = Object.freeze(new Set(['signature', 'seq', 'checkpoint']));
5
+
6
+ function normalizeReadClass(value) {
7
+ if (!value) return 'full-fold';
8
+ if (READ_CLASSES.has(value)) return value;
9
+ throw new Error(`Invalid readClass "${value}". Must be one of: window, head, full-fold.`);
10
+ }
11
+
12
+ function normalizeIntegrityClass(value) {
13
+ if (!value) return 'signature';
14
+ if (INTEGRITY_CLASSES.has(value)) return value;
15
+ throw new Error(`Invalid integrityClass "${value}". Must be one of: signature, seq, checkpoint.`);
16
+ }
17
+
18
+ function streamKey({ pluginId, collection, scopeType, scopeId, recordId } = {}) {
19
+ const col = nonEmptyString(collection, 'collection');
20
+ if (scopeType && scopeId) {
21
+ return recordId ? `${scopeType}:${scopeId}:${col}:${recordId}` : `${scopeType}:${scopeId}:${col}`;
22
+ }
23
+ const plugin = nonEmptyString(pluginId, 'pluginId');
24
+ return recordId ? `${plugin}:${col}:${recordId}` : `${plugin}:${col}`;
25
+ }
26
+
27
+ module.exports = { normalizeReadClass, normalizeIntegrityClass, streamKey };