@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 +7 -0
- package/README.md +124 -0
- package/dist/id.d.ts +36 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +129 -0
- package/dist/id.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/schema.d.ts +44 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +100 -0
- package/dist/schema.js.map +1 -0
- package/dist/stack.d.ts +187 -0
- package/dist/stack.d.ts.map +1 -0
- package/dist/stack.js +429 -0
- package/dist/stack.js.map +1 -0
- package/dist/types.d.ts +216 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +28 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +92 -0
- package/dist/validate.js.map +1 -0
- package/package.json +42 -0
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
|
package/dist/id.d.ts.map
ADDED
|
@@ -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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/schema.d.ts
ADDED
|
@@ -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":"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"}
|