@haverstack/core 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.
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
2
+
3
+ To the extent possible under law, the Haverstack contributors have waived all
4
+ copyright and related or neighboring rights to Haverstack. This work is
5
+ published from the United States.
6
+
7
+ https://creativecommons.org/publicdomain/zero/1.0/
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # @haverstack/core
2
+
3
+ Core library for Haverstack — a portable personal data stack.
4
+
5
+ Apps write **Records** into a **Stack**, and the stack handles storage, querying, versioning, and associations, regardless of where data actually lives. Switch backends without changing your app.
6
+
7
+ > **Status:** Early development. APIs are unstable.
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ npm install @haverstack/core
13
+ ```
14
+
15
+ You'll also need a storage adapter:
16
+
17
+ - [`@haverstack/adapter-sqlite`](https://www.npmjs.com/package/@haverstack/adapter-sqlite) — local SQLite storage via sql.js
18
+
19
+ ## Quick start
20
+
21
+ ```ts
22
+ import { Stack } from '@haverstack/core';
23
+ import { SQLiteAdapter } from '@haverstack/adapter-sqlite';
24
+
25
+ // First run — initialize a new stack
26
+ const adapter = await SQLiteAdapter.initialize({
27
+ path: './my-stack.db',
28
+ entityId: 'my-entity-id',
29
+ timezone: 'America/New_York',
30
+ });
31
+
32
+ // Subsequent runs — open the existing stack
33
+ // const adapter = await SQLiteAdapter.open({ path: './my-stack.db' });
34
+
35
+ const stack = await Stack.create(adapter);
36
+
37
+ // Define a type
38
+ await stack.defineType('com.example.myapp/note@1', 'Note', {
39
+ text: { kind: 'text', required: true },
40
+ title: { kind: 'string' },
41
+ });
42
+
43
+ // Create a record
44
+ const note = await stack.create('com.example.myapp/note@1', {
45
+ text: 'Hello, Haverstack!',
46
+ title: 'My first note',
47
+ });
48
+
49
+ // Update it (partial merge — only changed fields needed)
50
+ await stack.update(note.id, { title: 'Updated title' });
51
+
52
+ // Tag it
53
+ await stack.associate(note.id, { kind: 'tag', label: 'favourite' });
54
+
55
+ // Query
56
+ const notes = await stack.query({
57
+ filter: { typeId: 'com.example.myapp/note@1', tags: ['favourite'] },
58
+ sort: { field: 'createdAt', direction: 'desc' },
59
+ });
60
+
61
+ // Tear down when done
62
+ await stack.flush();
63
+ await stack.close();
64
+ ```
65
+
66
+ ## Core concepts
67
+
68
+ ### Records
69
+
70
+ The fundamental unit of data. Every record has:
71
+
72
+ - A **Crockford base-32 ID** — time-sortable, human-readable, URL-safe
73
+ - A **type** — defined by the app that created it
74
+ - **Content** — a JSON object validated against the type's schema
75
+ - Optional: `parentId`, `entityId`, `appId`, `permissions`, `associations`
76
+
77
+ ### Types
78
+
79
+ Types define the schema for a record's content. They are identified by a namespaced, versioned string:
80
+
81
+ ```
82
+ com.example.myapp/note@1
83
+ ```
84
+
85
+ The app author controls the namespace. Two stacks running the same app have the same type IDs and can interop.
86
+
87
+ ### Associations
88
+
89
+ Tags, attachments, and relationships are unified under a single model:
90
+
91
+ ```ts
92
+ { kind: 'tag', label: 'favourite' }
93
+ { kind: 'attachment', label: 'avatar', fileId: '...', mimeType: 'image/png' }
94
+ { kind: 'relationship', label: 'reply-to', recordId: '...' }
95
+ ```
96
+
97
+ ### Migrations
98
+
99
+ Types can evolve over time. Register migration functions between adjacent versions and the library composes them into chains automatically:
100
+
101
+ ```ts
102
+ await stack.defineType(
103
+ 'com.example.myapp/note@2',
104
+ 'Note',
105
+ { text: { kind: 'text', required: true }, title: { kind: 'string' } },
106
+ { migratesFrom: 'com.example.myapp/note@1' },
107
+ );
108
+
109
+ stack.registerMigration({
110
+ from: 'com.example.myapp/note@1',
111
+ to: 'com.example.myapp/note@2',
112
+ migrate: (content) => ({ ...content, title: '' }),
113
+ });
114
+ ```
115
+
116
+ Migration is **lazy** — records are migrated in memory on read and committed to disk on the next update. Use `stack.migrateAll()` to commit eagerly.
117
+
118
+ ## License
119
+
120
+ [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/) — public domain.
121
+
122
+ ## Monorepo
123
+
124
+ Part of [haverstack/core](https://github.com/haverstack/core).
package/dist/id.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Stack — ID Generation
3
+ * -------------------------------------------------------
4
+ * Crockford base-32 encoded IDs. Time-sortable, human-readable,
5
+ * and URL-safe. Unique within a stack.
6
+ *
7
+ * Format: 9-char timestamp prefix + 3-char random suffix = 12 chars total.
8
+ *
9
+ * Ported from https://github.com/cuibonobo/cuibonobo.com/blob/main/src/lib/id.ts
10
+ * with crypto.randomInt replaced by crypto.getRandomValues for
11
+ * runtime-agnostic compatibility (Node, browser, Deno, etc.)
12
+ */
13
+ export declare const BASE: number;
14
+ export declare const RAND_SUFFIX_LENGTH = 3;
15
+ export declare class IdGenerationError extends Error {
16
+ constructor(message?: string);
17
+ }
18
+ export declare class IdGenerationOverflowError extends IdGenerationError {
19
+ constructor(message?: string);
20
+ }
21
+ export declare const crockford32Encode: (n: number) => string;
22
+ export declare const crockford32Decode: (s: string) => number;
23
+ export declare const _setLastNowId: (chars: string) => void;
24
+ export declare const _setLastRandChars: (chars: string) => void;
25
+ /**
26
+ * Generate a new Stack record ID.
27
+ *
28
+ * IDs are time-sortable: lexicographic order matches creation order.
29
+ * Same-millisecond IDs are monotonically incremented to preserve order
30
+ * and avoid collisions. Throws IdGenerationOverflowError if more than
31
+ * BASE^RAND_SUFFIX_LENGTH (32^3 = 32,768) IDs are generated in one millisecond.
32
+ *
33
+ * @param timestamp - Override the timestamp (ms since epoch). Defaults to Date.now().
34
+ */
35
+ export declare const generateId: (timestamp?: number) => string;
36
+ //# sourceMappingURL=id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../src/id.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,eAAO,MAAM,IAAI,QAAoB,CAAC;AAGtC,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAUpC,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,OAAO,SAAK;CAIzB;AAED,qBAAa,yBAA0B,SAAQ,iBAAiB;gBAClD,OAAO,SAAK;CAIzB;AAMD,eAAO,MAAM,iBAAiB,GAAI,GAAG,MAAM,KAAG,MAiB7C,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,GAAG,MAAM,KAAG,MAe7C,CAAC;AAoCF,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,IAK7C,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,OAAO,MAAM,KAAG,IAKjD,CAAC;AAMF;;;;;;;;;GASG;AACH,eAAO,MAAM,UAAU,GAAI,YAAW,MAAmB,KAAG,MAQ3D,CAAC"}
package/dist/id.js ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Stack — ID Generation
3
+ * -------------------------------------------------------
4
+ * Crockford base-32 encoded IDs. Time-sortable, human-readable,
5
+ * and URL-safe. Unique within a stack.
6
+ *
7
+ * Format: 9-char timestamp prefix + 3-char random suffix = 12 chars total.
8
+ *
9
+ * Ported from https://github.com/cuibonobo/cuibonobo.com/blob/main/src/lib/id.ts
10
+ * with crypto.randomInt replaced by crypto.getRandomValues for
11
+ * runtime-agnostic compatibility (Node, browser, Deno, etc.)
12
+ */
13
+ const CHARACTERS = '0123456789abcdefghjkmnpqrstvwxyz';
14
+ export const BASE = CHARACTERS.length;
15
+ const MIN_TIMESTAMP_LENGTH = 9;
16
+ export const RAND_SUFFIX_LENGTH = 3;
17
+ // Module-level state for monotonicity within the same millisecond
18
+ let lastNowId = '';
19
+ let lastRandChars = '';
20
+ // -------------------------------------------------------
21
+ // Errors
22
+ // -------------------------------------------------------
23
+ export class IdGenerationError extends Error {
24
+ constructor(message = '') {
25
+ super(message || 'An ID could not be generated.');
26
+ this.name = 'IdGenerationError';
27
+ }
28
+ }
29
+ export class IdGenerationOverflowError extends IdGenerationError {
30
+ constructor(message = '') {
31
+ super(message || 'Too many IDs have been generated in the same millisecond.');
32
+ this.name = 'IdGenerationOverflowError';
33
+ }
34
+ }
35
+ // -------------------------------------------------------
36
+ // Encoding / decoding
37
+ // -------------------------------------------------------
38
+ export const crockford32Encode = (n) => {
39
+ if (n < 0) {
40
+ throw new RangeError('Not defined for negative numbers!');
41
+ }
42
+ n = Math.floor(n);
43
+ if (n === 0) {
44
+ return CHARACTERS[0];
45
+ }
46
+ let result = '';
47
+ while (n > 0) {
48
+ result = CHARACTERS[n % BASE] + result;
49
+ n = Math.floor(n / BASE);
50
+ }
51
+ return result;
52
+ };
53
+ export const crockford32Decode = (s) => {
54
+ if (s.length === 0) {
55
+ throw new RangeError('String must not be empty!');
56
+ }
57
+ s = s.toLowerCase();
58
+ let n = 0;
59
+ for (let i = 0; i < s.length; i++) {
60
+ const val = CHARACTERS.indexOf(s[s.length - i - 1]);
61
+ if (val < 0) {
62
+ throw new RangeError(`Undefined character in string: "${s[s.length - i - 1]}"`);
63
+ }
64
+ n += val * Math.pow(BASE, i);
65
+ }
66
+ return n;
67
+ };
68
+ // -------------------------------------------------------
69
+ // Internal helpers
70
+ // -------------------------------------------------------
71
+ const pad = (chars, length) => chars.padStart(length, CHARACTERS[0]);
72
+ /**
73
+ * Generate a random suffix using Web Crypto API.
74
+ * Works in Node (>=19), browsers, Deno, and Bun.
75
+ */
76
+ const generateRandChars = () => {
77
+ const max = Math.pow(BASE, RAND_SUFFIX_LENGTH) - 1;
78
+ const arr = new Uint32Array(1);
79
+ // Rejection sampling to avoid modulo bias
80
+ let value;
81
+ do {
82
+ crypto.getRandomValues(arr);
83
+ value = arr[0];
84
+ } while (value > Math.floor(0xffffffff / max) * max);
85
+ return pad(crockford32Encode(value % max), RAND_SUFFIX_LENGTH);
86
+ };
87
+ const incrementRandChars = (randChars) => {
88
+ const next = crockford32Encode(crockford32Decode(randChars) + 1);
89
+ if (next.length > RAND_SUFFIX_LENGTH) {
90
+ throw new IdGenerationOverflowError();
91
+ }
92
+ return pad(next, RAND_SUFFIX_LENGTH);
93
+ };
94
+ // -------------------------------------------------------
95
+ // Test hooks (package-private)
96
+ // -------------------------------------------------------
97
+ export const _setLastNowId = (chars) => {
98
+ if (chars.length < MIN_TIMESTAMP_LENGTH) {
99
+ throw new RangeError(`lastNowId must have at least ${MIN_TIMESTAMP_LENGTH} characters.`);
100
+ }
101
+ lastNowId = chars;
102
+ };
103
+ export const _setLastRandChars = (chars) => {
104
+ if (chars.length !== RAND_SUFFIX_LENGTH) {
105
+ throw new RangeError(`lastRandChars must have exactly ${RAND_SUFFIX_LENGTH} characters.`);
106
+ }
107
+ lastRandChars = chars;
108
+ };
109
+ // -------------------------------------------------------
110
+ // Public API
111
+ // -------------------------------------------------------
112
+ /**
113
+ * Generate a new Stack record ID.
114
+ *
115
+ * IDs are time-sortable: lexicographic order matches creation order.
116
+ * Same-millisecond IDs are monotonically incremented to preserve order
117
+ * and avoid collisions. Throws IdGenerationOverflowError if more than
118
+ * BASE^RAND_SUFFIX_LENGTH (32^3 = 32,768) IDs are generated in one millisecond.
119
+ *
120
+ * @param timestamp - Override the timestamp (ms since epoch). Defaults to Date.now().
121
+ */
122
+ export const generateId = (timestamp = Date.now()) => {
123
+ const nowId = pad(crockford32Encode(timestamp), MIN_TIMESTAMP_LENGTH);
124
+ const randChars = nowId !== lastNowId ? generateRandChars() : incrementRandChars(lastRandChars);
125
+ lastNowId = nowId;
126
+ lastRandChars = randChars;
127
+ return nowId + randChars;
128
+ };
129
+ //# sourceMappingURL=id.js.map
package/dist/id.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"id.js","sourceRoot":"","sources":["../src/id.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @haverstack/core
3
+ * -------------------------------------------------------
4
+ * Core library for Haverstack — portable personal data stack.
5
+ *
6
+ * Exports the Stack class, all types, and utility functions.
7
+ * Storage adapters are published as separate packages:
8
+ * @haverstack/adapter-sqlite
9
+ */
10
+ export { Stack, StackValidationError, StackMigrationError } from './stack.js';
11
+ export type { CreateRecordOptions, GetRecordOptions, DeleteRecordOptions } from './stack.js';
12
+ export type { RecordId, TypeId, FileId, StackRecord, RecordVersion, StackType, TypeSchema, FieldDef, ScalarFieldDef, ArrayFieldDef, ObjectFieldDef, ScalarFieldKind, Association, TagAssociation, AttachmentAssociation, RelationshipAssociation, Permission, StackQuery, RecordFilter, QuerySort, QueryResult, DateRange, Migration, MigrationFn, AdapterCapabilities, StackAdapter, EntityContent, AppContent, GroupContent, } from './types.js';
13
+ export { SYSTEM_TYPES } from './types.js';
14
+ export { generateId, crockford32Encode, crockford32Decode } from './id.js';
15
+ export { hashSchema, isCompatible, parseTypeId, buildTypeId } from './schema.js';
16
+ export { validateContent, isValid } from './validate.js';
17
+ export type { ValidationError } from './validate.js';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAC9E,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAG7F,YAAY,EACV,QAAQ,EACR,MAAM,EACN,MAAM,EACN,WAAW,EACX,aAAa,EACb,SAAS,EACT,UAAU,EACV,QAAQ,EACR,cAAc,EACd,aAAa,EACb,cAAc,EACd,eAAe,EACf,WAAW,EACX,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACvB,UAAU,EACV,UAAU,EACV,YAAY,EACZ,SAAS,EACT,WAAW,EACX,SAAS,EACT,SAAS,EACT,WAAW,EACX,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,UAAU,EACV,YAAY,GACb,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG1C,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAC3E,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACzD,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @haverstack/core
3
+ * -------------------------------------------------------
4
+ * Core library for Haverstack — portable personal data stack.
5
+ *
6
+ * Exports the Stack class, all types, and utility functions.
7
+ * Storage adapters are published as separate packages:
8
+ * @haverstack/adapter-sqlite
9
+ */
10
+ // Core class
11
+ export { Stack, StackValidationError, StackMigrationError } from './stack.js';
12
+ export { SYSTEM_TYPES } from './types.js';
13
+ // Utilities
14
+ export { generateId, crockford32Encode, crockford32Decode } from './id.js';
15
+ export { hashSchema, isCompatible, parseTypeId, buildTypeId } from './schema.js';
16
+ export { validateContent, isValid } from './validate.js';
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,aAAa;AACb,OAAO,EAAE,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAoC9E,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,YAAY;AACZ,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAC3E,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Stack — Schema Utilities
3
+ * -------------------------------------------------------
4
+ * Schema hashing and type compatibility checks.
5
+ *
6
+ * Hashing produces a stable SHA-256 fingerprint of a TypeSchema
7
+ * by canonicalizing it first (alphabetical keys, minified JSON).
8
+ * This is used for drift detection — not type identity, which is
9
+ * the namespaced ID controlled by the app author.
10
+ */
11
+ import type { TypeSchema } from './types.js';
12
+ /**
13
+ * Compute a stable SHA-256 hash of a TypeSchema.
14
+ * Used for drift detection — if two records share a typeId but their
15
+ * schemas hash differently, the schema was mutated without a version bump.
16
+ */
17
+ export declare const hashSchema: (schema: TypeSchema) => Promise<string>;
18
+ /**
19
+ * Check whether a candidate schema satisfies a required schema.
20
+ *
21
+ * A candidate is compatible if it contains all *required* fields from
22
+ * the required schema with matching kinds. Optional fields in the
23
+ * required schema are ignored. Array and object fields are matched
24
+ * shallowly — only the top-level kind is checked.
25
+ *
26
+ * Apps that need precise type matching should compare typeIds directly.
27
+ * isCompatible() is for duck-typed consumption across types.
28
+ */
29
+ export declare const isCompatible: (candidateSchema: TypeSchema, requiredSchema: TypeSchema) => boolean;
30
+ /**
31
+ * Parse a versioned TypeId into its base and version components.
32
+ * e.g. "com.example.myapp/note@2" → { baseId: "com.example.myapp/note", version: 2 }
33
+ * Returns null if the ID is not versioned (e.g. system types at definition time).
34
+ */
35
+ export declare const parseTypeId: (typeId: string) => {
36
+ baseId: string;
37
+ version: number;
38
+ } | null;
39
+ /**
40
+ * Build a versioned TypeId from a base ID and version number.
41
+ * e.g. ("com.example.myapp/note", 2) → "com.example.myapp/note@2"
42
+ */
43
+ export declare const buildTypeId: (baseId: string, version: number) => string;
44
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAY,MAAM,YAAY,CAAC;AA8CvD;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAU,QAAQ,UAAU,KAAG,OAAO,CAAC,MAAM,CAMnE,CAAC;AAMF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,GAAI,iBAAiB,UAAU,EAAE,gBAAgB,UAAU,KAAG,OAMtF,CAAC;AAMF;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,KAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAIlF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,EAAE,SAAS,MAAM,KAAG,MAAgC,CAAC"}
package/dist/schema.js ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Stack — Schema Utilities
3
+ * -------------------------------------------------------
4
+ * Schema hashing and type compatibility checks.
5
+ *
6
+ * Hashing produces a stable SHA-256 fingerprint of a TypeSchema
7
+ * by canonicalizing it first (alphabetical keys, minified JSON).
8
+ * This is used for drift detection — not type identity, which is
9
+ * the namespaced ID controlled by the app author.
10
+ */
11
+ // -------------------------------------------------------
12
+ // Canonical schema serialization
13
+ // -------------------------------------------------------
14
+ /**
15
+ * Recursively sort all object keys alphabetically so that two schemas
16
+ * with the same fields in different orders produce the same hash.
17
+ */
18
+ const canonicalizeFieldDef = (def) => {
19
+ if (def.kind === 'array') {
20
+ return {
21
+ items: canonicalizeFieldDef(def.items),
22
+ kind: def.kind,
23
+ ...(def.required !== undefined && { required: def.required }),
24
+ };
25
+ }
26
+ if (def.kind === 'object') {
27
+ return {
28
+ kind: def.kind,
29
+ properties: canonicalizeSchema(def.properties),
30
+ ...(def.required !== undefined && { required: def.required }),
31
+ };
32
+ }
33
+ // Scalar
34
+ return {
35
+ kind: def.kind,
36
+ ...(def.required !== undefined && { required: def.required }),
37
+ };
38
+ };
39
+ const canonicalizeSchema = (schema) => {
40
+ return Object.fromEntries(Object.keys(schema)
41
+ .sort()
42
+ .map((key) => [key, canonicalizeFieldDef(schema[key])]));
43
+ };
44
+ // -------------------------------------------------------
45
+ // Hashing
46
+ // -------------------------------------------------------
47
+ /**
48
+ * Compute a stable SHA-256 hash of a TypeSchema.
49
+ * Used for drift detection — if two records share a typeId but their
50
+ * schemas hash differently, the schema was mutated without a version bump.
51
+ */
52
+ export const hashSchema = async (schema) => {
53
+ const canonical = JSON.stringify(canonicalizeSchema(schema));
54
+ const buffer = new TextEncoder().encode(canonical);
55
+ const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
56
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
57
+ return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
58
+ };
59
+ // -------------------------------------------------------
60
+ // Type compatibility
61
+ // -------------------------------------------------------
62
+ /**
63
+ * Check whether a candidate schema satisfies a required schema.
64
+ *
65
+ * A candidate is compatible if it contains all *required* fields from
66
+ * the required schema with matching kinds. Optional fields in the
67
+ * required schema are ignored. Array and object fields are matched
68
+ * shallowly — only the top-level kind is checked.
69
+ *
70
+ * Apps that need precise type matching should compare typeIds directly.
71
+ * isCompatible() is for duck-typed consumption across types.
72
+ */
73
+ export const isCompatible = (candidateSchema, requiredSchema) => {
74
+ return Object.entries(requiredSchema).every(([key, def]) => {
75
+ if (!def.required)
76
+ return true;
77
+ const field = candidateSchema[key];
78
+ return field !== undefined && field.kind === def.kind;
79
+ });
80
+ };
81
+ // -------------------------------------------------------
82
+ // Type ID parsing
83
+ // -------------------------------------------------------
84
+ /**
85
+ * Parse a versioned TypeId into its base and version components.
86
+ * e.g. "com.example.myapp/note@2" → { baseId: "com.example.myapp/note", version: 2 }
87
+ * Returns null if the ID is not versioned (e.g. system types at definition time).
88
+ */
89
+ export const parseTypeId = (typeId) => {
90
+ const match = typeId.match(/^(.+)@(\d+)$/);
91
+ if (!match)
92
+ return null;
93
+ return { baseId: match[1], version: parseInt(match[2], 10) };
94
+ };
95
+ /**
96
+ * Build a versioned TypeId from a base ID and version number.
97
+ * e.g. ("com.example.myapp/note", 2) → "com.example.myapp/note@2"
98
+ */
99
+ export const buildTypeId = (baseId, version) => `${baseId}@${version}`;
100
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,0DAA0D;AAC1D,iCAAiC;AACjC,0DAA0D;AAE1D;;;GAGG;AACH,MAAM,oBAAoB,GAAG,CAAC,GAAa,EAAW,EAAE;IACtD,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO;YACL,KAAK,EAAE,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC;YACtC,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,GAAG,CAAC,GAAG,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;SAC9D,CAAC;IACJ,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO;YACL,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,UAAU,EAAE,kBAAkB,CAAC,GAAG,CAAC,UAAU,CAAC;YAC9C,GAAG,CAAC,GAAG,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;SAC9D,CAAC;IACJ,CAAC;IAED,SAAS;IACT,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,GAAG,CAAC,GAAG,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;KAC9D,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,kBAAkB,GAAG,CAAC,MAAkB,EAAW,EAAE;IACzD,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;SAChB,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,oBAAoB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAC1D,CAAC;AACJ,CAAC,CAAC;AAEF,0DAA0D;AAC1D,UAAU;AACV,0DAA0D;AAE1D;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAAE,MAAkB,EAAmB,EAAE;IACtE,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7D,MAAM,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACnD,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACjE,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC;IACzD,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACxE,CAAC,CAAC;AAEF,0DAA0D;AAC1D,qBAAqB;AACrB,0DAA0D;AAE1D;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,eAA2B,EAAE,cAA0B,EAAW,EAAE;IAC/F,OAAO,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,EAAE;QACzD,IAAI,CAAC,GAAG,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC/B,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;QACnC,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC;IACxD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,0DAA0D;AAC1D,kBAAkB;AAClB,0DAA0D;AAE1D;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAc,EAA8C,EAAE;IACxF,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IAC3C,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AAC/D,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAc,EAAE,OAAe,EAAU,EAAE,CAAC,GAAG,MAAM,IAAI,OAAO,EAAE,CAAC"}