@objectstack/metadata-core 5.0.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 +202 -0
- package/README.md +40 -0
- package/dist/chunk-F3VZBGKC.cjs +91 -0
- package/dist/chunk-F3VZBGKC.cjs.map +1 -0
- package/dist/chunk-G3VZZW54.js +91 -0
- package/dist/chunk-G3VZZW54.js.map +1 -0
- package/dist/index.cjs +611 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +248 -0
- package/dist/index.d.ts +248 -0
- package/dist/index.js +611 -0
- package/dist/index.js.map +1 -0
- package/dist/repository-DJhCkVYK.d.cts +307 -0
- package/dist/repository-DJhCkVYK.d.ts +307 -0
- package/dist/testing.cjs +254 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +10 -0
- package/dist/testing.d.ts +10 -0
- package/dist/testing.js +254 -0
- package/dist/testing.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Metadata Repository types — see ADR-0008 §2.
|
|
5
|
+
*
|
|
6
|
+
* All shapes are defined as Zod schemas so the same definition serves
|
|
7
|
+
* runtime validation and static typing (`z.infer<typeof X>`).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Canonical metadata type names. Aligned with the `MetadataTypeSchema`
|
|
12
|
+
* enum in `@objectstack/spec/kernel/metadata-plugin.zod.ts`. New types are
|
|
13
|
+
* added here in lockstep with that file.
|
|
14
|
+
*/
|
|
15
|
+
declare const MetadataTypeSchema: z.ZodEnum<{
|
|
16
|
+
object: "object";
|
|
17
|
+
view: "view";
|
|
18
|
+
page: "page";
|
|
19
|
+
dashboard: "dashboard";
|
|
20
|
+
app: "app";
|
|
21
|
+
flow: "flow";
|
|
22
|
+
workflow: "workflow";
|
|
23
|
+
agent: "agent";
|
|
24
|
+
tool: "tool";
|
|
25
|
+
skill: "skill";
|
|
26
|
+
report: "report";
|
|
27
|
+
translation: "translation";
|
|
28
|
+
role: "role";
|
|
29
|
+
permission: "permission";
|
|
30
|
+
policy: "policy";
|
|
31
|
+
api: "api";
|
|
32
|
+
endpoint: "endpoint";
|
|
33
|
+
datasource: "datasource";
|
|
34
|
+
cube: "cube";
|
|
35
|
+
settings: "settings";
|
|
36
|
+
}>;
|
|
37
|
+
type MetadataType = z.infer<typeof MetadataTypeSchema>;
|
|
38
|
+
/**
|
|
39
|
+
* Fully-qualified reference to a metadata item. Identity is `(org, type, name)`.
|
|
40
|
+
*
|
|
41
|
+
* Per ADR-0008 v2 (2026-05) the metadata layer no longer carries `project`
|
|
42
|
+
* or `branch`. Project survives only as an **artifact packaging concept**
|
|
43
|
+
* (the unit a CLI/CI run compiles into `dist/objectstack.json`); it does
|
|
44
|
+
* not appear in the runtime customization scope. Branching belongs to Git
|
|
45
|
+
* (or your VCS of choice) and never propagated cleanly into the runtime
|
|
46
|
+
* model — so it has been removed entirely.
|
|
47
|
+
*
|
|
48
|
+
* Higher layers may default `org='system'` for built-ins.
|
|
49
|
+
*
|
|
50
|
+
* `version` is optional: omit to mean "HEAD", supply to pin.
|
|
51
|
+
*/
|
|
52
|
+
declare const MetaRefSchema: z.ZodObject<{
|
|
53
|
+
org: z.ZodString;
|
|
54
|
+
type: z.ZodEnum<{
|
|
55
|
+
object: "object";
|
|
56
|
+
view: "view";
|
|
57
|
+
page: "page";
|
|
58
|
+
dashboard: "dashboard";
|
|
59
|
+
app: "app";
|
|
60
|
+
flow: "flow";
|
|
61
|
+
workflow: "workflow";
|
|
62
|
+
agent: "agent";
|
|
63
|
+
tool: "tool";
|
|
64
|
+
skill: "skill";
|
|
65
|
+
report: "report";
|
|
66
|
+
translation: "translation";
|
|
67
|
+
role: "role";
|
|
68
|
+
permission: "permission";
|
|
69
|
+
policy: "policy";
|
|
70
|
+
api: "api";
|
|
71
|
+
endpoint: "endpoint";
|
|
72
|
+
datasource: "datasource";
|
|
73
|
+
cube: "cube";
|
|
74
|
+
settings: "settings";
|
|
75
|
+
}>;
|
|
76
|
+
name: z.ZodString;
|
|
77
|
+
version: z.ZodOptional<z.ZodString>;
|
|
78
|
+
}, z.core.$strip>;
|
|
79
|
+
type MetaRef = z.infer<typeof MetaRefSchema>;
|
|
80
|
+
/**
|
|
81
|
+
* Construct a stable string key from a MetaRef (excluding `version`,
|
|
82
|
+
* which is mutable). Used as cache keys and log indexes.
|
|
83
|
+
*/
|
|
84
|
+
declare function refKey(ref: Pick<MetaRef, 'org' | 'type' | 'name'>): string;
|
|
85
|
+
/**
|
|
86
|
+
* Full metadata item as stored / returned by the Repository.
|
|
87
|
+
*
|
|
88
|
+
* `body` is the **canonical, Zod-normalised** spec (with defaults filled
|
|
89
|
+
* in). `hash` is `sha256(canonicalize(body))`. Equal hashes imply equal
|
|
90
|
+
* specs.
|
|
91
|
+
*/
|
|
92
|
+
declare const MetadataItemSchema: z.ZodObject<{
|
|
93
|
+
ref: z.ZodObject<{
|
|
94
|
+
org: z.ZodString;
|
|
95
|
+
type: z.ZodEnum<{
|
|
96
|
+
object: "object";
|
|
97
|
+
view: "view";
|
|
98
|
+
page: "page";
|
|
99
|
+
dashboard: "dashboard";
|
|
100
|
+
app: "app";
|
|
101
|
+
flow: "flow";
|
|
102
|
+
workflow: "workflow";
|
|
103
|
+
agent: "agent";
|
|
104
|
+
tool: "tool";
|
|
105
|
+
skill: "skill";
|
|
106
|
+
report: "report";
|
|
107
|
+
translation: "translation";
|
|
108
|
+
role: "role";
|
|
109
|
+
permission: "permission";
|
|
110
|
+
policy: "policy";
|
|
111
|
+
api: "api";
|
|
112
|
+
endpoint: "endpoint";
|
|
113
|
+
datasource: "datasource";
|
|
114
|
+
cube: "cube";
|
|
115
|
+
settings: "settings";
|
|
116
|
+
}>;
|
|
117
|
+
name: z.ZodString;
|
|
118
|
+
version: z.ZodOptional<z.ZodString>;
|
|
119
|
+
}, z.core.$strip>;
|
|
120
|
+
body: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
121
|
+
hash: z.ZodString;
|
|
122
|
+
parentHash: z.ZodNullable<z.ZodString>;
|
|
123
|
+
authoredBy: z.ZodString;
|
|
124
|
+
authoredAt: z.ZodString;
|
|
125
|
+
message: z.ZodOptional<z.ZodString>;
|
|
126
|
+
seq: z.ZodNumber;
|
|
127
|
+
schemaVersion: z.ZodOptional<z.ZodString>;
|
|
128
|
+
}, z.core.$strip>;
|
|
129
|
+
type MetadataItem = z.infer<typeof MetadataItemSchema>;
|
|
130
|
+
/** Lightweight header for listing — `body` omitted. */
|
|
131
|
+
type MetadataItemHeader = Omit<MetadataItem, 'body'>;
|
|
132
|
+
declare const MetadataOpSchema: z.ZodEnum<{
|
|
133
|
+
create: "create";
|
|
134
|
+
update: "update";
|
|
135
|
+
delete: "delete";
|
|
136
|
+
rename: "rename";
|
|
137
|
+
}>;
|
|
138
|
+
type MetadataOp = z.infer<typeof MetadataOpSchema>;
|
|
139
|
+
/**
|
|
140
|
+
* The single event payload broadcast by the change log. ADR-0008 §2.4.
|
|
141
|
+
*
|
|
142
|
+
* For `rename`, `previousName` carries the old machine name. For
|
|
143
|
+
* `delete`, `hash` is null. The payload is intentionally small —
|
|
144
|
+
* consumers re-fetch via the cache when they need the full body.
|
|
145
|
+
*/
|
|
146
|
+
declare const MetadataEventSchema: z.ZodObject<{
|
|
147
|
+
seq: z.ZodNumber;
|
|
148
|
+
op: z.ZodEnum<{
|
|
149
|
+
create: "create";
|
|
150
|
+
update: "update";
|
|
151
|
+
delete: "delete";
|
|
152
|
+
rename: "rename";
|
|
153
|
+
}>;
|
|
154
|
+
ref: z.ZodObject<{
|
|
155
|
+
org: z.ZodString;
|
|
156
|
+
type: z.ZodEnum<{
|
|
157
|
+
object: "object";
|
|
158
|
+
view: "view";
|
|
159
|
+
page: "page";
|
|
160
|
+
dashboard: "dashboard";
|
|
161
|
+
app: "app";
|
|
162
|
+
flow: "flow";
|
|
163
|
+
workflow: "workflow";
|
|
164
|
+
agent: "agent";
|
|
165
|
+
tool: "tool";
|
|
166
|
+
skill: "skill";
|
|
167
|
+
report: "report";
|
|
168
|
+
translation: "translation";
|
|
169
|
+
role: "role";
|
|
170
|
+
permission: "permission";
|
|
171
|
+
policy: "policy";
|
|
172
|
+
api: "api";
|
|
173
|
+
endpoint: "endpoint";
|
|
174
|
+
datasource: "datasource";
|
|
175
|
+
cube: "cube";
|
|
176
|
+
settings: "settings";
|
|
177
|
+
}>;
|
|
178
|
+
name: z.ZodString;
|
|
179
|
+
version: z.ZodOptional<z.ZodString>;
|
|
180
|
+
}, z.core.$strip>;
|
|
181
|
+
hash: z.ZodNullable<z.ZodString>;
|
|
182
|
+
parentHash: z.ZodNullable<z.ZodString>;
|
|
183
|
+
previousName: z.ZodOptional<z.ZodString>;
|
|
184
|
+
actor: z.ZodString;
|
|
185
|
+
message: z.ZodOptional<z.ZodString>;
|
|
186
|
+
ts: z.ZodString;
|
|
187
|
+
source: z.ZodString;
|
|
188
|
+
}, z.core.$strip>;
|
|
189
|
+
type MetadataEvent = z.infer<typeof MetadataEventSchema>;
|
|
190
|
+
interface PutOptions {
|
|
191
|
+
/**
|
|
192
|
+
* Hash this writer believed was at HEAD. `null` means "creating, expect
|
|
193
|
+
* absence". A mismatch throws ConflictError.
|
|
194
|
+
*/
|
|
195
|
+
parentVersion: string | null;
|
|
196
|
+
/** Identity of the writer; mirrored to MetadataEvent.actor. */
|
|
197
|
+
actor: string;
|
|
198
|
+
/** Optional human-readable commit message. */
|
|
199
|
+
message?: string;
|
|
200
|
+
/** Optional label for the change log "source" column. */
|
|
201
|
+
source?: string;
|
|
202
|
+
}
|
|
203
|
+
interface PutResult {
|
|
204
|
+
/** New content hash assigned to the spec. */
|
|
205
|
+
version: string;
|
|
206
|
+
/** Sequence number of the emitted MetadataEvent. */
|
|
207
|
+
seq: number;
|
|
208
|
+
/** The committed item (canonicalised). */
|
|
209
|
+
item: MetadataItem;
|
|
210
|
+
}
|
|
211
|
+
interface DeleteOptions {
|
|
212
|
+
parentVersion: string;
|
|
213
|
+
actor: string;
|
|
214
|
+
message?: string;
|
|
215
|
+
source?: string;
|
|
216
|
+
}
|
|
217
|
+
interface DeleteResult {
|
|
218
|
+
seq: number;
|
|
219
|
+
}
|
|
220
|
+
interface ListFilter {
|
|
221
|
+
org?: string;
|
|
222
|
+
type?: MetadataType;
|
|
223
|
+
/** Substring match on `name`; case-sensitive. */
|
|
224
|
+
nameContains?: string;
|
|
225
|
+
/** Pagination cursor; opaque string from a previous response. */
|
|
226
|
+
cursor?: string;
|
|
227
|
+
/** Page size; implementations may clamp. */
|
|
228
|
+
limit?: number;
|
|
229
|
+
}
|
|
230
|
+
interface WatchFilter {
|
|
231
|
+
org?: string;
|
|
232
|
+
type?: MetadataType;
|
|
233
|
+
/** When omitted, match all names within the scope. */
|
|
234
|
+
name?: string;
|
|
235
|
+
}
|
|
236
|
+
interface HistoryOptions {
|
|
237
|
+
/** Lower bound (exclusive) for pagination. */
|
|
238
|
+
sinceSeq?: number;
|
|
239
|
+
limit?: number;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The `MetadataRepository` interface — single point of pluggability for
|
|
244
|
+
* the metadata storage backend. See ADR-0008 §2.6.
|
|
245
|
+
*
|
|
246
|
+
* Implementations:
|
|
247
|
+
*
|
|
248
|
+
* - `InMemoryRepository` (this package, for tests & edge)
|
|
249
|
+
* - `FileSystemRepository` (`@objectstack/metadata`)
|
|
250
|
+
* - `LayeredRepository` (`@objectstack/metadata`)
|
|
251
|
+
* - `PostgresRepository` (`@objectstack/metadata-postgres`, M1)
|
|
252
|
+
*
|
|
253
|
+
* Implementation contract — what every backend MUST guarantee:
|
|
254
|
+
*
|
|
255
|
+
* 1. **Atomic put.** A successful `put()` either fully applies (item
|
|
256
|
+
* visible to subsequent `get` AND an event present in the log) or
|
|
257
|
+
* does not apply at all. No half-states.
|
|
258
|
+
* 2. **Monotonic seq per org.** `seq` is strictly increasing within
|
|
259
|
+
* `org`. Different orgs have independent sequences. (Repositories
|
|
260
|
+
* scoped to a single org may treat the entire repo as one log.)
|
|
261
|
+
* 3. **Optimistic locking.** `put` and `delete` throw `ConflictError`
|
|
262
|
+
* when `parentVersion` does not match the current HEAD.
|
|
263
|
+
* 4. **Canonical hashing.** `item.hash === hashSpec(item.body)` — always.
|
|
264
|
+
* 5. **Event ordering.** Subscribers to `watch()` receive events in
|
|
265
|
+
* monotonically-increasing `seq` order with no gaps.
|
|
266
|
+
* 6. **Resumability.** `watch(_, since)` MUST replay all events with
|
|
267
|
+
* `seq > since` before delivering live events.
|
|
268
|
+
* 7. **Tombstones, not holes.** `delete` produces a `delete` event;
|
|
269
|
+
* `get` returns null but `history` still shows the lineage.
|
|
270
|
+
*/
|
|
271
|
+
|
|
272
|
+
interface MetadataRepository {
|
|
273
|
+
/** Read HEAD or a pinned version. Returns null if absent. */
|
|
274
|
+
get(ref: MetaRef): Promise<MetadataItem | null>;
|
|
275
|
+
/**
|
|
276
|
+
* Write a new version. Atomic.
|
|
277
|
+
* @throws ConflictError if `parentVersion` does not match HEAD.
|
|
278
|
+
* @throws SchemaValidationError if `spec` fails Zod normalisation.
|
|
279
|
+
*/
|
|
280
|
+
put(ref: MetaRef, spec: unknown, opts: PutOptions): Promise<PutResult>;
|
|
281
|
+
/**
|
|
282
|
+
* Soft-delete (tombstone). `parentVersion` is required.
|
|
283
|
+
* @throws ConflictError on parent mismatch.
|
|
284
|
+
*/
|
|
285
|
+
delete(ref: MetaRef, opts: DeleteOptions): Promise<DeleteResult>;
|
|
286
|
+
/** Enumerate items matching a filter. Implementations may stream. */
|
|
287
|
+
list(filter: ListFilter): AsyncIterable<MetadataItemHeader>;
|
|
288
|
+
/** Per-item history; events in monotonic `seq` order. */
|
|
289
|
+
history(ref: MetaRef, opts?: HistoryOptions): AsyncIterable<MetadataEvent>;
|
|
290
|
+
/**
|
|
291
|
+
* Live event stream. The iterator MUST:
|
|
292
|
+
*
|
|
293
|
+
* - Replay all events with `seq > since` before yielding any new event.
|
|
294
|
+
* - Stay open until the consumer breaks the loop.
|
|
295
|
+
* - Survive transient backend disconnects (implementation's choice
|
|
296
|
+
* how to resume — Postgres LISTEN reconnect, JSONL tail, etc.).
|
|
297
|
+
*/
|
|
298
|
+
watch(filter: WatchFilter, since?: number): AsyncIterable<MetadataEvent>;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Sentinel symbol used by `LayeredRepository` (M0 PR-5) to label which
|
|
302
|
+
* underlying layer emitted an event. Defined here so the contract is
|
|
303
|
+
* shared.
|
|
304
|
+
*/
|
|
305
|
+
declare const LAYER_SOURCE: unique symbol;
|
|
306
|
+
|
|
307
|
+
export { type DeleteOptions as D, type HistoryOptions as H, LAYER_SOURCE as L, type MetaRef as M, type PutOptions as P, type WatchFilter as W, type DeleteResult as a, type ListFilter as b, MetaRefSchema as c, type MetadataEvent as d, MetadataEventSchema as e, type MetadataItem as f, type MetadataItemHeader as g, MetadataItemSchema as h, type MetadataOp as i, MetadataOpSchema as j, type MetadataRepository as k, type MetadataType as l, MetadataTypeSchema as m, type PutResult as n, refKey as r };
|
package/dist/testing.cjs
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
var _chunkF3VZBGKCcjs = require('./chunk-F3VZBGKC.cjs');
|
|
5
|
+
|
|
6
|
+
// src/contract-suite.ts
|
|
7
|
+
var _vitest = require('vitest');
|
|
8
|
+
var refOf = (overrides = {}) => ({
|
|
9
|
+
org: "system",
|
|
10
|
+
type: "view",
|
|
11
|
+
name: "sample_view",
|
|
12
|
+
...overrides
|
|
13
|
+
});
|
|
14
|
+
var spec = (label) => ({ label, columns: ["a", "b"] });
|
|
15
|
+
async function take(iter, n, timeoutMs = 1e3) {
|
|
16
|
+
const out = [];
|
|
17
|
+
const it2 = iter[Symbol.asyncIterator]();
|
|
18
|
+
const deadline = Date.now() + timeoutMs;
|
|
19
|
+
while (out.length < n) {
|
|
20
|
+
const remaining = deadline - Date.now();
|
|
21
|
+
if (remaining <= 0) break;
|
|
22
|
+
const result = await Promise.race([
|
|
23
|
+
it2.next(),
|
|
24
|
+
new Promise(
|
|
25
|
+
(resolve) => setTimeout(() => resolve({ value: void 0, done: true }), remaining)
|
|
26
|
+
)
|
|
27
|
+
]);
|
|
28
|
+
if (result.done) break;
|
|
29
|
+
out.push(result.value);
|
|
30
|
+
}
|
|
31
|
+
await _optionalChain([it2, 'access', _ => _.return, 'optionalCall', _2 => _2(void 0)]);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
function runRepositoryContractTests(label, factory, opts = {}) {
|
|
35
|
+
_vitest.describe.call(void 0, `MetadataRepository contract \u2014 ${label}`, () => {
|
|
36
|
+
_vitest.describe.call(void 0, "put / get", () => {
|
|
37
|
+
_vitest.it.call(void 0, "creates an item from null parent", async () => {
|
|
38
|
+
const repo = await factory();
|
|
39
|
+
const ref = refOf();
|
|
40
|
+
const res = await repo.put(ref, spec("hello"), { parentVersion: null, actor: "tester" });
|
|
41
|
+
_vitest.expect.call(void 0, res.version).toMatch(/^sha256:[0-9a-f]{64}$/);
|
|
42
|
+
_vitest.expect.call(void 0, res.version).toBe(_chunkF3VZBGKCcjs.hashSpec.call(void 0, spec("hello")));
|
|
43
|
+
_vitest.expect.call(void 0, res.seq).toBeGreaterThan(0);
|
|
44
|
+
_vitest.expect.call(void 0, res.item.parentHash).toBeNull();
|
|
45
|
+
_vitest.expect.call(void 0, res.item.authoredBy).toBe("tester");
|
|
46
|
+
const got = await repo.get(ref);
|
|
47
|
+
_vitest.expect.call(void 0, got).not.toBeNull();
|
|
48
|
+
_vitest.expect.call(void 0, got.hash).toBe(res.version);
|
|
49
|
+
_vitest.expect.call(void 0, got.body).toEqual(spec("hello"));
|
|
50
|
+
});
|
|
51
|
+
_vitest.it.call(void 0, "round-trips successive updates with parent chaining", async () => {
|
|
52
|
+
const repo = await factory();
|
|
53
|
+
const ref = refOf();
|
|
54
|
+
const a = await repo.put(ref, spec("one"), { parentVersion: null, actor: "t" });
|
|
55
|
+
const b = await repo.put(ref, spec("two"), { parentVersion: a.version, actor: "t" });
|
|
56
|
+
const c = await repo.put(ref, spec("three"), { parentVersion: b.version, actor: "t" });
|
|
57
|
+
_vitest.expect.call(void 0, b.item.parentHash).toBe(a.version);
|
|
58
|
+
_vitest.expect.call(void 0, c.item.parentHash).toBe(b.version);
|
|
59
|
+
_vitest.expect.call(void 0, c.seq).toBeGreaterThan(b.seq);
|
|
60
|
+
_vitest.expect.call(void 0, b.seq).toBeGreaterThan(a.seq);
|
|
61
|
+
});
|
|
62
|
+
_vitest.it.call(void 0, "canonical hash invariant: item.hash === hashSpec(item.body)", async () => {
|
|
63
|
+
const repo = await factory();
|
|
64
|
+
const ref = refOf();
|
|
65
|
+
await repo.put(ref, { z: 1, a: 2, m: [3, 1, 2] }, { parentVersion: null, actor: "t" });
|
|
66
|
+
const got = await repo.get(ref);
|
|
67
|
+
_vitest.expect.call(void 0, got.hash).toBe(_chunkF3VZBGKCcjs.hashSpec.call(void 0, got.body));
|
|
68
|
+
});
|
|
69
|
+
_vitest.it.call(void 0, "returns null for missing item", async () => {
|
|
70
|
+
const repo = await factory();
|
|
71
|
+
_vitest.expect.call(void 0, await repo.get(refOf({ name: "never_existed" }))).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
_vitest.it.call(void 0, "no-op write with identical content returns current version", async () => {
|
|
74
|
+
const repo = await factory();
|
|
75
|
+
const ref = refOf();
|
|
76
|
+
const a = await repo.put(ref, spec("same"), { parentVersion: null, actor: "t" });
|
|
77
|
+
const b = await repo.put(ref, spec("same"), { parentVersion: a.version, actor: "t" });
|
|
78
|
+
_vitest.expect.call(void 0, b.version).toBe(a.version);
|
|
79
|
+
_vitest.expect.call(void 0, b.seq).toBe(a.seq);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
_vitest.describe.call(void 0, "optimistic locking", () => {
|
|
83
|
+
_vitest.it.call(void 0, "throws ConflictError when parentVersion mismatches", async () => {
|
|
84
|
+
const repo = await factory();
|
|
85
|
+
const ref = refOf();
|
|
86
|
+
const a = await repo.put(ref, spec("v1"), { parentVersion: null, actor: "t" });
|
|
87
|
+
await _vitest.expect.call(void 0,
|
|
88
|
+
repo.put(ref, spec("v2"), { parentVersion: null, actor: "t" })
|
|
89
|
+
).rejects.toBeInstanceOf(_chunkF3VZBGKCcjs.ConflictError);
|
|
90
|
+
await _vitest.expect.call(void 0,
|
|
91
|
+
repo.put(ref, spec("v2"), { parentVersion: "sha256:deadbeef".padEnd(71, "0"), actor: "t" })
|
|
92
|
+
).rejects.toBeInstanceOf(_chunkF3VZBGKCcjs.ConflictError);
|
|
93
|
+
await _vitest.expect.call(void 0,
|
|
94
|
+
repo.put(ref, spec("v2"), { parentVersion: a.version, actor: "t" })
|
|
95
|
+
).resolves.toMatchObject({ seq: _vitest.expect.any(Number) });
|
|
96
|
+
});
|
|
97
|
+
_vitest.it.call(void 0, "throws ConflictError when creating over an existing item with null parent", async () => {
|
|
98
|
+
const repo = await factory();
|
|
99
|
+
const ref = refOf();
|
|
100
|
+
await repo.put(ref, spec("a"), { parentVersion: null, actor: "t" });
|
|
101
|
+
await _vitest.expect.call(void 0,
|
|
102
|
+
repo.put(ref, spec("b"), { parentVersion: null, actor: "t" })
|
|
103
|
+
).rejects.toBeInstanceOf(_chunkF3VZBGKCcjs.ConflictError);
|
|
104
|
+
});
|
|
105
|
+
_vitest.it.call(void 0, "delete requires correct parentVersion", async () => {
|
|
106
|
+
const repo = await factory();
|
|
107
|
+
const ref = refOf();
|
|
108
|
+
const a = await repo.put(ref, spec("a"), { parentVersion: null, actor: "t" });
|
|
109
|
+
await _vitest.expect.call(void 0,
|
|
110
|
+
repo.delete(ref, { parentVersion: "sha256:wrong".padEnd(71, "0"), actor: "t" })
|
|
111
|
+
).rejects.toBeInstanceOf(_chunkF3VZBGKCcjs.ConflictError);
|
|
112
|
+
await repo.delete(ref, { parentVersion: a.version, actor: "t" });
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
_vitest.describe.call(void 0, "monotonic seq per org", () => {
|
|
116
|
+
_vitest.it.call(void 0, "seq strictly increases within an org", async () => {
|
|
117
|
+
const repo = await factory();
|
|
118
|
+
const ref = refOf();
|
|
119
|
+
const a = await repo.put(ref, spec("1"), { parentVersion: null, actor: "t" });
|
|
120
|
+
const b = await repo.put(ref, spec("2"), { parentVersion: a.version, actor: "t" });
|
|
121
|
+
const c = await repo.put(refOf({ name: "other" }), spec("o"), { parentVersion: null, actor: "t" });
|
|
122
|
+
_vitest.expect.call(void 0, b.seq).toBeGreaterThan(a.seq);
|
|
123
|
+
_vitest.expect.call(void 0, c.seq).toBeGreaterThan(b.seq);
|
|
124
|
+
});
|
|
125
|
+
_vitest.it.call(void 0, "different orgs have independent sequences", async () => {
|
|
126
|
+
const repo = await factory();
|
|
127
|
+
const orgA = refOf({ org: "org_a" });
|
|
128
|
+
const orgB = refOf({ org: "org_b" });
|
|
129
|
+
let a;
|
|
130
|
+
let b;
|
|
131
|
+
try {
|
|
132
|
+
a = await repo.put(orgA, spec("a1"), { parentVersion: null, actor: "t" });
|
|
133
|
+
b = await repo.put(orgB, spec("b1"), { parentVersion: null, actor: "t" });
|
|
134
|
+
} catch (e2) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const c = await repo.put(orgA, spec("a2"), { parentVersion: a.version, actor: "t" });
|
|
138
|
+
const d = await repo.put(orgB, spec("b2"), { parentVersion: b.version, actor: "t" });
|
|
139
|
+
_vitest.expect.call(void 0, c.seq).toBeGreaterThan(a.seq);
|
|
140
|
+
_vitest.expect.call(void 0, d.seq).toBeGreaterThan(b.seq);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
_vitest.describe.call(void 0, "delete / tombstones", () => {
|
|
144
|
+
_vitest.it.call(void 0, "get returns null after delete; history retains lineage", async () => {
|
|
145
|
+
const repo = await factory();
|
|
146
|
+
const ref = refOf();
|
|
147
|
+
const a = await repo.put(ref, spec("a"), { parentVersion: null, actor: "t" });
|
|
148
|
+
await repo.delete(ref, { parentVersion: a.version, actor: "t" });
|
|
149
|
+
_vitest.expect.call(void 0, await repo.get(ref)).toBeNull();
|
|
150
|
+
const hist = [];
|
|
151
|
+
for await (const evt of repo.history(ref)) hist.push(evt);
|
|
152
|
+
_vitest.expect.call(void 0, hist.map((e) => e.op)).toEqual(["create", "delete"]);
|
|
153
|
+
_vitest.expect.call(void 0, _optionalChain([hist, 'access', _3 => _3[1], 'optionalAccess', _4 => _4.parentHash])).toBe(a.version);
|
|
154
|
+
_vitest.expect.call(void 0, _optionalChain([hist, 'access', _5 => _5[1], 'optionalAccess', _6 => _6.hash])).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
_vitest.it.call(void 0, "can recreate after delete with null parent", async () => {
|
|
157
|
+
const repo = await factory();
|
|
158
|
+
const ref = refOf();
|
|
159
|
+
const a = await repo.put(ref, spec("a"), { parentVersion: null, actor: "t" });
|
|
160
|
+
await repo.delete(ref, { parentVersion: a.version, actor: "t" });
|
|
161
|
+
const b = await repo.put(ref, spec("a-redux"), { parentVersion: null, actor: "t" });
|
|
162
|
+
_vitest.expect.call(void 0, b.item.parentHash).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
_vitest.describe.call(void 0, "watch / history", () => {
|
|
166
|
+
_vitest.it.call(void 0, "history yields events in monotonic seq order", async () => {
|
|
167
|
+
const repo = await factory();
|
|
168
|
+
const ref = refOf();
|
|
169
|
+
const a = await repo.put(ref, spec("1"), { parentVersion: null, actor: "t" });
|
|
170
|
+
const b = await repo.put(ref, spec("2"), { parentVersion: a.version, actor: "t" });
|
|
171
|
+
const c = await repo.put(ref, spec("3"), { parentVersion: b.version, actor: "t" });
|
|
172
|
+
const evts = [];
|
|
173
|
+
for await (const e of repo.history(ref)) evts.push(e);
|
|
174
|
+
_vitest.expect.call(void 0, evts.map((e) => e.seq)).toEqual([a.seq, b.seq, c.seq]);
|
|
175
|
+
_vitest.expect.call(void 0, evts.every((e, i) => i === 0 || e.seq > evts[i - 1].seq)).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
_vitest.it.call(void 0, "watch(sinceSeq) replays subsequent events then goes live", async () => {
|
|
178
|
+
const repo = await factory();
|
|
179
|
+
const ref = refOf();
|
|
180
|
+
const a = await repo.put(ref, spec("1"), { parentVersion: null, actor: "t" });
|
|
181
|
+
const b = await repo.put(ref, spec("2"), { parentVersion: a.version, actor: "t" });
|
|
182
|
+
const iter = repo.watch({ org: ref.org }, a.seq);
|
|
183
|
+
const collected = [];
|
|
184
|
+
const it2 = iter[Symbol.asyncIterator]();
|
|
185
|
+
const first = await it2.next();
|
|
186
|
+
_vitest.expect.call(void 0, first.done).toBe(false);
|
|
187
|
+
collected.push(first.value);
|
|
188
|
+
_vitest.expect.call(void 0, collected[0].seq).toBe(b.seq);
|
|
189
|
+
const livePromise = it2.next();
|
|
190
|
+
const c = await repo.put(ref, spec("3"), { parentVersion: b.version, actor: "t" });
|
|
191
|
+
const live = await livePromise;
|
|
192
|
+
_vitest.expect.call(void 0, live.done).toBe(false);
|
|
193
|
+
collected.push(live.value);
|
|
194
|
+
_vitest.expect.call(void 0, collected[1].seq).toBe(c.seq);
|
|
195
|
+
await _optionalChain([it2, 'access', _7 => _7.return, 'optionalCall', _8 => _8(void 0)]);
|
|
196
|
+
});
|
|
197
|
+
_vitest.it.call(void 0, "watch filters by type and name", async () => {
|
|
198
|
+
const repo = await factory();
|
|
199
|
+
await repo.put(refOf({ name: "a" }), spec("a"), { parentVersion: null, actor: "t" });
|
|
200
|
+
await repo.put(refOf({ name: "b" }), spec("b"), { parentVersion: null, actor: "t" });
|
|
201
|
+
const events = await take(
|
|
202
|
+
repo.watch({ org: "system", type: "view", name: "a" }),
|
|
203
|
+
5,
|
|
204
|
+
200
|
|
205
|
+
);
|
|
206
|
+
_vitest.expect.call(void 0, events.length).toBe(1);
|
|
207
|
+
_vitest.expect.call(void 0, events[0].ref.name).toBe("a");
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
_vitest.describe.call(void 0, "list", () => {
|
|
211
|
+
_vitest.it.call(void 0, "returns headers (no body) for matching items", async () => {
|
|
212
|
+
const repo = await factory();
|
|
213
|
+
await repo.put(refOf({ name: "alpha" }), spec("a"), { parentVersion: null, actor: "t" });
|
|
214
|
+
await repo.put(refOf({ name: "beta" }), spec("b"), { parentVersion: null, actor: "t" });
|
|
215
|
+
await repo.put(refOf({ type: "object", name: "thing" }), spec("o"), {
|
|
216
|
+
parentVersion: null,
|
|
217
|
+
actor: "t"
|
|
218
|
+
});
|
|
219
|
+
const headers = [];
|
|
220
|
+
for await (const h of repo.list({ type: "view" })) headers.push(h);
|
|
221
|
+
_vitest.expect.call(void 0, headers.length).toBe(2);
|
|
222
|
+
for (const h of headers) {
|
|
223
|
+
_vitest.expect.call(void 0, h.body).toBeUndefined();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
_vitest.it.call(void 0, "limit clamps result size", async () => {
|
|
227
|
+
const repo = await factory();
|
|
228
|
+
for (let i = 0; i < 5; i++) {
|
|
229
|
+
await repo.put(refOf({ name: `v_${i}` }), spec(`v${i}`), { parentVersion: null, actor: "t" });
|
|
230
|
+
}
|
|
231
|
+
const headers = [];
|
|
232
|
+
for await (const h of repo.list({ type: "view", limit: 3 })) headers.push(h);
|
|
233
|
+
_vitest.expect.call(void 0, headers.length).toBe(3);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
if (opts.supportsVersionedReads) {
|
|
237
|
+
_vitest.describe.call(void 0, "versioned reads", () => {
|
|
238
|
+
_vitest.it.call(void 0, "get with version pin returns that historical version", async () => {
|
|
239
|
+
const repo = await factory();
|
|
240
|
+
const ref = refOf();
|
|
241
|
+
const a = await repo.put(ref, spec("v1"), { parentVersion: null, actor: "t" });
|
|
242
|
+
await repo.put(ref, spec("v2"), { parentVersion: a.version, actor: "t" });
|
|
243
|
+
const pinned = await repo.get({ ...ref, version: a.version });
|
|
244
|
+
_vitest.expect.call(void 0, _optionalChain([pinned, 'optionalAccess', _9 => _9.hash])).toBe(a.version);
|
|
245
|
+
_vitest.expect.call(void 0, _optionalChain([pinned, 'optionalAccess', _10 => _10.body])).toEqual(spec("v1"));
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
exports.runRepositoryContractTests = runRepositoryContractTests;
|
|
254
|
+
//# sourceMappingURL=testing.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/framework/framework/packages/metadata-core/dist/testing.cjs","../src/contract-suite.ts"],"names":["it"],"mappings":"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","file":"/home/runner/work/framework/framework/packages/metadata-core/dist/testing.cjs","sourcesContent":[null,"// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Parameterised Repository contract test suite. Every `MetadataRepository`\n * implementation MUST pass this suite. Reuse:\n *\n * import { runRepositoryContractTests } from '@objectstack/metadata-core/test';\n * runRepositoryContractTests('InMemory', () => new InMemoryRepository());\n *\n * The suite verifies the seven invariants from `repository.ts`:\n *\n * 1. Atomic put\n * 2. Monotonic seq per branch\n * 3. Optimistic locking (ConflictError)\n * 4. Canonical hashing (hash === hashSpec(body))\n * 5. Event ordering (monotonic seq, no gaps)\n * 6. Resumability (watch with `since` replays)\n * 7. Tombstones (delete event emitted, get returns null)\n */\n\nimport { describe, it, expect } from 'vitest';\nimport type { MetadataRepository } from './repository.js';\nimport type { MetaRef, MetadataEvent } from './types.js';\nimport { hashSpec } from './canonicalize.js';\nimport { ConflictError } from './errors.js';\n\nexport interface ContractSuiteOptions {\n /** If the implementation supports `version`-pinned reads, set true. */\n supportsVersionedReads?: boolean;\n}\n\nconst refOf = (overrides: Partial<MetaRef> = {}): MetaRef => ({\n org: 'system',\n type: 'view',\n name: 'sample_view',\n ...overrides,\n});\n\nconst spec = (label: string) => ({ label, columns: ['a', 'b'] });\n\n/** Drain at most `n` events from an async iterable with a timeout. */\nasync function take<T>(iter: AsyncIterable<T>, n: number, timeoutMs = 1000): Promise<T[]> {\n const out: T[] = [];\n const it = iter[Symbol.asyncIterator]();\n const deadline = Date.now() + timeoutMs;\n while (out.length < n) {\n const remaining = deadline - Date.now();\n if (remaining <= 0) break;\n const result = await Promise.race([\n it.next(),\n new Promise<{ value: undefined; done: true }>((resolve) =>\n setTimeout(() => resolve({ value: undefined, done: true }), remaining),\n ),\n ]);\n if (result.done) break;\n out.push(result.value as T);\n }\n // Close the iterator so the repo subscriber is freed.\n await it.return?.(undefined);\n return out;\n}\n\nexport function runRepositoryContractTests(\n label: string,\n factory: () => MetadataRepository | Promise<MetadataRepository>,\n opts: ContractSuiteOptions = {},\n): void {\n describe(`MetadataRepository contract — ${label}`, () => {\n // ── 1. Atomic put + canonical hash ──────────────────────────────\n describe('put / get', () => {\n it('creates an item from null parent', async () => {\n const repo = await factory();\n const ref = refOf();\n const res = await repo.put(ref, spec('hello'), { parentVersion: null, actor: 'tester' });\n expect(res.version).toMatch(/^sha256:[0-9a-f]{64}$/);\n expect(res.version).toBe(hashSpec(spec('hello')));\n expect(res.seq).toBeGreaterThan(0);\n expect(res.item.parentHash).toBeNull();\n expect(res.item.authoredBy).toBe('tester');\n\n const got = await repo.get(ref);\n expect(got).not.toBeNull();\n expect(got!.hash).toBe(res.version);\n expect(got!.body).toEqual(spec('hello'));\n });\n\n it('round-trips successive updates with parent chaining', async () => {\n const repo = await factory();\n const ref = refOf();\n const a = await repo.put(ref, spec('one'), { parentVersion: null, actor: 't' });\n const b = await repo.put(ref, spec('two'), { parentVersion: a.version, actor: 't' });\n const c = await repo.put(ref, spec('three'), { parentVersion: b.version, actor: 't' });\n expect(b.item.parentHash).toBe(a.version);\n expect(c.item.parentHash).toBe(b.version);\n expect(c.seq).toBeGreaterThan(b.seq);\n expect(b.seq).toBeGreaterThan(a.seq);\n });\n\n it('canonical hash invariant: item.hash === hashSpec(item.body)', async () => {\n const repo = await factory();\n const ref = refOf();\n await repo.put(ref, { z: 1, a: 2, m: [3, 1, 2] }, { parentVersion: null, actor: 't' });\n const got = await repo.get(ref);\n expect(got!.hash).toBe(hashSpec(got!.body));\n });\n\n it('returns null for missing item', async () => {\n const repo = await factory();\n expect(await repo.get(refOf({ name: 'never_existed' }))).toBeNull();\n });\n\n it('no-op write with identical content returns current version', async () => {\n const repo = await factory();\n const ref = refOf();\n const a = await repo.put(ref, spec('same'), { parentVersion: null, actor: 't' });\n const b = await repo.put(ref, spec('same'), { parentVersion: a.version, actor: 't' });\n expect(b.version).toBe(a.version);\n expect(b.seq).toBe(a.seq);\n });\n });\n\n // ── 2 & 3. Optimistic locking + Monotonic seq ───────────────────\n describe('optimistic locking', () => {\n it('throws ConflictError when parentVersion mismatches', async () => {\n const repo = await factory();\n const ref = refOf();\n const a = await repo.put(ref, spec('v1'), { parentVersion: null, actor: 't' });\n await expect(\n repo.put(ref, spec('v2'), { parentVersion: null, actor: 't' }),\n ).rejects.toBeInstanceOf(ConflictError);\n await expect(\n repo.put(ref, spec('v2'), { parentVersion: 'sha256:deadbeef'.padEnd(71, '0'), actor: 't' }),\n ).rejects.toBeInstanceOf(ConflictError);\n // Sanity: correct parent succeeds.\n await expect(\n repo.put(ref, spec('v2'), { parentVersion: a.version, actor: 't' }),\n ).resolves.toMatchObject({ seq: expect.any(Number) });\n });\n\n it('throws ConflictError when creating over an existing item with null parent', async () => {\n const repo = await factory();\n const ref = refOf();\n await repo.put(ref, spec('a'), { parentVersion: null, actor: 't' });\n await expect(\n repo.put(ref, spec('b'), { parentVersion: null, actor: 't' }),\n ).rejects.toBeInstanceOf(ConflictError);\n });\n\n it('delete requires correct parentVersion', async () => {\n const repo = await factory();\n const ref = refOf();\n const a = await repo.put(ref, spec('a'), { parentVersion: null, actor: 't' });\n await expect(\n repo.delete(ref, { parentVersion: 'sha256:wrong'.padEnd(71, '0'), actor: 't' }),\n ).rejects.toBeInstanceOf(ConflictError);\n await repo.delete(ref, { parentVersion: a.version, actor: 't' });\n });\n });\n\n describe('monotonic seq per org', () => {\n it('seq strictly increases within an org', async () => {\n const repo = await factory();\n const ref = refOf();\n const a = await repo.put(ref, spec('1'), { parentVersion: null, actor: 't' });\n const b = await repo.put(ref, spec('2'), { parentVersion: a.version, actor: 't' });\n const c = await repo.put(refOf({ name: 'other' }), spec('o'), { parentVersion: null, actor: 't' });\n expect(b.seq).toBeGreaterThan(a.seq);\n expect(c.seq).toBeGreaterThan(b.seq);\n });\n\n it('different orgs have independent sequences', async () => {\n const repo = await factory();\n const orgA = refOf({ org: 'org_a' });\n const orgB = refOf({ org: 'org_b' });\n // Some backends (FileSystemRepository) are scoped to a single\n // org; for those any foreign-org put throws — skip the test.\n let a: { seq: number; version: string };\n let b: { seq: number; version: string };\n try {\n a = await repo.put(orgA, spec('a1'), { parentVersion: null, actor: 't' });\n b = await repo.put(orgB, spec('b1'), { parentVersion: null, actor: 't' });\n } catch {\n return;\n }\n const c = await repo.put(orgA, spec('a2'), { parentVersion: a.version, actor: 't' });\n const d = await repo.put(orgB, spec('b2'), { parentVersion: b.version, actor: 't' });\n expect(c.seq).toBeGreaterThan(a.seq);\n expect(d.seq).toBeGreaterThan(b.seq);\n });\n });\n\n // ── 4. Tombstones ───────────────────────────────────────────────\n describe('delete / tombstones', () => {\n it('get returns null after delete; history retains lineage', async () => {\n const repo = await factory();\n const ref = refOf();\n const a = await repo.put(ref, spec('a'), { parentVersion: null, actor: 't' });\n await repo.delete(ref, { parentVersion: a.version, actor: 't' });\n expect(await repo.get(ref)).toBeNull();\n const hist: MetadataEvent[] = [];\n for await (const evt of repo.history(ref)) hist.push(evt);\n expect(hist.map((e) => e.op)).toEqual(['create', 'delete']);\n expect(hist[1]?.parentHash).toBe(a.version);\n expect(hist[1]?.hash).toBeNull();\n });\n\n it('can recreate after delete with null parent', async () => {\n const repo = await factory();\n const ref = refOf();\n const a = await repo.put(ref, spec('a'), { parentVersion: null, actor: 't' });\n await repo.delete(ref, { parentVersion: a.version, actor: 't' });\n const b = await repo.put(ref, spec('a-redux'), { parentVersion: null, actor: 't' });\n expect(b.item.parentHash).toBeNull();\n });\n });\n\n // ── 5. Event ordering & watch replay ────────────────────────────\n describe('watch / history', () => {\n it('history yields events in monotonic seq order', async () => {\n const repo = await factory();\n const ref = refOf();\n const a = await repo.put(ref, spec('1'), { parentVersion: null, actor: 't' });\n const b = await repo.put(ref, spec('2'), { parentVersion: a.version, actor: 't' });\n const c = await repo.put(ref, spec('3'), { parentVersion: b.version, actor: 't' });\n const evts: MetadataEvent[] = [];\n for await (const e of repo.history(ref)) evts.push(e);\n expect(evts.map((e) => e.seq)).toEqual([a.seq, b.seq, c.seq]);\n expect(evts.every((e, i) => i === 0 || e.seq > evts[i - 1]!.seq)).toBe(true);\n });\n\n it('watch(sinceSeq) replays subsequent events then goes live', async () => {\n const repo = await factory();\n const ref = refOf();\n const a = await repo.put(ref, spec('1'), { parentVersion: null, actor: 't' });\n const b = await repo.put(ref, spec('2'), { parentVersion: a.version, actor: 't' });\n\n // Start watching with `since = a.seq` — must replay b, then deliver a live event.\n const iter = repo.watch({ org: ref.org }, a.seq);\n const collected: MetadataEvent[] = [];\n const it = iter[Symbol.asyncIterator]();\n\n // First yield should be the replay of `b`.\n const first = await it.next();\n expect(first.done).toBe(false);\n collected.push(first.value as MetadataEvent);\n expect(collected[0]!.seq).toBe(b.seq);\n\n // Now trigger a live event and collect it.\n const livePromise = it.next();\n const c = await repo.put(ref, spec('3'), { parentVersion: b.version, actor: 't' });\n const live = await livePromise;\n expect(live.done).toBe(false);\n collected.push(live.value as MetadataEvent);\n expect(collected[1]!.seq).toBe(c.seq);\n\n await it.return?.(undefined);\n });\n\n it('watch filters by type and name', async () => {\n const repo = await factory();\n await repo.put(refOf({ name: 'a' }), spec('a'), { parentVersion: null, actor: 't' });\n await repo.put(refOf({ name: 'b' }), spec('b'), { parentVersion: null, actor: 't' });\n const events = await take(\n repo.watch({ org: 'system', type: 'view', name: 'a' }),\n 5,\n 200,\n );\n expect(events.length).toBe(1);\n expect(events[0]!.ref.name).toBe('a');\n });\n });\n\n // ── list ────────────────────────────────────────────────────────\n describe('list', () => {\n it('returns headers (no body) for matching items', async () => {\n const repo = await factory();\n await repo.put(refOf({ name: 'alpha' }), spec('a'), { parentVersion: null, actor: 't' });\n await repo.put(refOf({ name: 'beta' }), spec('b'), { parentVersion: null, actor: 't' });\n await repo.put(refOf({ type: 'object', name: 'thing' }), spec('o'), {\n parentVersion: null,\n actor: 't',\n });\n const headers: unknown[] = [];\n for await (const h of repo.list({ type: 'view' })) headers.push(h);\n expect(headers.length).toBe(2);\n for (const h of headers) {\n expect((h as { body?: unknown }).body).toBeUndefined();\n }\n });\n\n it('limit clamps result size', async () => {\n const repo = await factory();\n for (let i = 0; i < 5; i++) {\n await repo.put(refOf({ name: `v_${i}` }), spec(`v${i}`), { parentVersion: null, actor: 't' });\n }\n const headers: unknown[] = [];\n for await (const h of repo.list({ type: 'view', limit: 3 })) headers.push(h);\n expect(headers.length).toBe(3);\n });\n });\n\n // ── Optional behaviour ──────────────────────────────────────────\n if (opts.supportsVersionedReads) {\n describe('versioned reads', () => {\n it('get with version pin returns that historical version', async () => {\n const repo = await factory();\n const ref = refOf();\n const a = await repo.put(ref, spec('v1'), { parentVersion: null, actor: 't' });\n await repo.put(ref, spec('v2'), { parentVersion: a.version, actor: 't' });\n const pinned = await repo.get({ ...ref, version: a.version });\n expect(pinned?.hash).toBe(a.version);\n expect(pinned?.body).toEqual(spec('v1'));\n });\n });\n }\n });\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { k as MetadataRepository } from './repository-DJhCkVYK.cjs';
|
|
2
|
+
import 'zod';
|
|
3
|
+
|
|
4
|
+
interface ContractSuiteOptions {
|
|
5
|
+
/** If the implementation supports `version`-pinned reads, set true. */
|
|
6
|
+
supportsVersionedReads?: boolean;
|
|
7
|
+
}
|
|
8
|
+
declare function runRepositoryContractTests(label: string, factory: () => MetadataRepository | Promise<MetadataRepository>, opts?: ContractSuiteOptions): void;
|
|
9
|
+
|
|
10
|
+
export { type ContractSuiteOptions, runRepositoryContractTests };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { k as MetadataRepository } from './repository-DJhCkVYK.js';
|
|
2
|
+
import 'zod';
|
|
3
|
+
|
|
4
|
+
interface ContractSuiteOptions {
|
|
5
|
+
/** If the implementation supports `version`-pinned reads, set true. */
|
|
6
|
+
supportsVersionedReads?: boolean;
|
|
7
|
+
}
|
|
8
|
+
declare function runRepositoryContractTests(label: string, factory: () => MetadataRepository | Promise<MetadataRepository>, opts?: ContractSuiteOptions): void;
|
|
9
|
+
|
|
10
|
+
export { type ContractSuiteOptions, runRepositoryContractTests };
|