@classytic/repo-core 0.3.0 → 0.4.1
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/CHANGELOG.md +243 -0
- package/dist/_virtual/_rolldown/runtime.mjs +7 -0
- package/dist/adapter/index.d.mts +3 -0
- package/dist/adapter/index.mjs +2 -0
- package/dist/adapter/types.d.mts +222 -0
- package/dist/adapter/widen.d.mts +22 -0
- package/dist/adapter/widen.mjs +26 -0
- package/dist/aggregate/index.d.mts +3 -0
- package/dist/aggregate/index.mjs +3 -0
- package/dist/aggregate/keyset.d.mts +57 -0
- package/dist/aggregate/keyset.mjs +45 -0
- package/dist/aggregate/normalize.d.mts +24 -0
- package/dist/aggregate/normalize.mjs +28 -0
- package/dist/better-auth/index.d.mts +110 -0
- package/dist/better-auth/index.mjs +71 -0
- package/dist/cache/engine.d.mts +127 -0
- package/dist/cache/engine.mjs +235 -0
- package/dist/cache/envelope.mjs +32 -0
- package/dist/cache/index.d.mts +7 -2
- package/dist/cache/index.mjs +6 -2
- package/dist/cache/keys.mjs +131 -0
- package/dist/cache/memory-adapter.mjs +41 -7
- package/dist/cache/options.d.mts +112 -0
- package/dist/cache/options.mjs +25 -0
- package/dist/cache/plugin/context.d.mts +18 -0
- package/dist/cache/plugin/context.mjs +121 -0
- package/dist/cache/plugin/index.d.mts +86 -0
- package/dist/cache/plugin/index.mjs +78 -0
- package/dist/cache/plugin/invalidation-hooks.mjs +35 -0
- package/dist/cache/plugin/read-hooks.mjs +96 -0
- package/dist/cache/plugin/swr.mjs +20 -0
- package/dist/cache/runtime.d.mts +43 -0
- package/dist/cache/runtime.mjs +14 -0
- package/dist/cache/tag-index.mjs +84 -0
- package/dist/cache/timeout-adapter.d.mts +30 -0
- package/dist/cache/timeout-adapter.mjs +58 -0
- package/dist/cache/types.d.mts +45 -0
- package/dist/cache/version-store.mjs +57 -0
- package/dist/errors/index.d.mts +2 -1
- package/dist/errors/index.mjs +2 -1
- package/dist/errors/schema.d.mts +101 -0
- package/dist/errors/schema.mjs +78 -0
- package/dist/filter/match.mjs +38 -2
- package/dist/lock/index.d.mts +132 -0
- package/dist/lock/index.mjs +162 -0
- package/dist/pagination/canonical.d.mts +8 -8
- package/dist/pagination/canonical.mjs +3 -9
- package/dist/pagination/cursor.mjs +4 -1
- package/dist/pagination/index.d.mts +2 -2
- package/dist/pagination/types.d.mts +17 -27
- package/dist/plugins/index.d.mts +2 -0
- package/dist/plugins/index.mjs +2 -0
- package/dist/plugins/tenant-helpers.d.mts +63 -0
- package/dist/plugins/tenant-helpers.mjs +84 -0
- package/dist/query-parser/index.d.mts +2 -1
- package/dist/query-parser/index.mjs +2 -1
- package/dist/query-parser/parse-url.mjs +13 -11
- package/dist/query-parser/reserved.d.mts +43 -0
- package/dist/query-parser/reserved.mjs +56 -0
- package/dist/repository/agg-output.d.mts +63 -0
- package/dist/repository/agg-output.mjs +89 -0
- package/dist/repository/index.d.mts +4 -2
- package/dist/repository/index.mjs +3 -1
- package/dist/repository/options.d.mts +62 -0
- package/dist/repository/options.mjs +57 -0
- package/dist/repository/types.d.mts +936 -49
- package/dist/schema/field-rules.d.mts +41 -1
- package/dist/schema/field-rules.mjs +92 -1
- package/dist/schema/index.d.mts +2 -2
- package/dist/schema/index.mjs +2 -2
- package/dist/schema/types.d.mts +21 -0
- package/dist/testing/conformance.mjs +666 -17
- package/dist/testing/index.d.mts +3 -2
- package/dist/testing/index.mjs +2 -1
- package/dist/testing/lock-conformance.d.mts +25 -0
- package/dist/testing/lock-conformance.mjs +167 -0
- package/dist/testing/types.d.mts +99 -2
- package/package.json +23 -1
- package/dist/cache/stable-stringify.d.mts +0 -15
package/dist/testing/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import { ConformanceContext, ConformanceDoc, ConformanceFeatures, ConformanceHarness } from "./types.mjs";
|
|
1
|
+
import { AggregateOpsSupport, ConformanceContext, ConformanceDoc, ConformanceFeatures, ConformanceHarness } from "./types.mjs";
|
|
2
2
|
import { runStandardRepoConformance } from "./conformance.mjs";
|
|
3
|
-
|
|
3
|
+
import { LockConformanceHarness, runLockAdapterConformance } from "./lock-conformance.mjs";
|
|
4
|
+
export { type AggregateOpsSupport, type ConformanceContext, type ConformanceDoc, type ConformanceFeatures, type ConformanceHarness, type LockConformanceHarness, runLockAdapterConformance, runStandardRepoConformance };
|
package/dist/testing/index.mjs
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LockAdapter } from "../lock/index.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/testing/lock-conformance.d.ts
|
|
4
|
+
interface LockConformanceHarness {
|
|
5
|
+
/**
|
|
6
|
+
* Construct a fresh adapter for each describe block. May be async
|
|
7
|
+
* (e.g. SQL adapters that need to run a CREATE TABLE migration).
|
|
8
|
+
* The same adapter instance is shared by every test in the suite —
|
|
9
|
+
* `beforeEach` is responsible for clearing residual lock state.
|
|
10
|
+
*/
|
|
11
|
+
createAdapter(): LockAdapter | Promise<LockAdapter>;
|
|
12
|
+
/**
|
|
13
|
+
* Wipe every lock between tests. Mongo: drop the collection.
|
|
14
|
+
* SQLite: `DELETE FROM kit_locks`. Memory: noop (factory returns
|
|
15
|
+
* a fresh `Map`).
|
|
16
|
+
*
|
|
17
|
+
* Required because tests share an adapter and acquire under
|
|
18
|
+
* conflicting names. A leaked lock from one test breaks the
|
|
19
|
+
* "first acquire wins" invariant in the next.
|
|
20
|
+
*/
|
|
21
|
+
beforeEach?(adapter: LockAdapter): void | Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
declare function runLockAdapterConformance(harness: LockConformanceHarness): void;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { LockConformanceHarness, runLockAdapterConformance };
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
//#region src/testing/lock-conformance.ts
|
|
3
|
+
/**
|
|
4
|
+
* `runLockAdapterConformance` — cross-kit lock-adapter contract suite.
|
|
5
|
+
*
|
|
6
|
+
* Wires a kit-specific `LockConformanceHarness` to a canonical set of
|
|
7
|
+
* scenarios that every `LockAdapter` implementation should pass. The
|
|
8
|
+
* goal is parity: "swap mongokit/lock for sqlitekit/lock" must be a
|
|
9
|
+
* provable claim, and behavior drift between backends shows up here
|
|
10
|
+
* before it ships.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors `runStandardRepoConformance` in shape — vitest is imported
|
|
13
|
+
* at top of file (this subpath is test-only) and the harness gives
|
|
14
|
+
* the kit one chance to construct the adapter, then handles cleanup
|
|
15
|
+
* between scenarios.
|
|
16
|
+
*
|
|
17
|
+
* ## Usage from a kit
|
|
18
|
+
*
|
|
19
|
+
* import { runLockAdapterConformance } from '@classytic/repo-core/testing';
|
|
20
|
+
* import { createMongoLockAdapter } from '../../src/lock/index.js';
|
|
21
|
+
*
|
|
22
|
+
* describe('mongokit/lock conformance', () => {
|
|
23
|
+
* runLockAdapterConformance({
|
|
24
|
+
* createAdapter: () => createMongoLockAdapter({ collectionName: 'lock_conformance' }),
|
|
25
|
+
* async beforeEach() { await clearLocks(); },
|
|
26
|
+
* });
|
|
27
|
+
* });
|
|
28
|
+
*/
|
|
29
|
+
const A = "replica-A";
|
|
30
|
+
const B = "replica-B";
|
|
31
|
+
function runLockAdapterConformance(harness) {
|
|
32
|
+
describe("LockAdapter contract", () => {
|
|
33
|
+
let adapter;
|
|
34
|
+
beforeEach(async () => {
|
|
35
|
+
adapter = await harness.createAdapter();
|
|
36
|
+
await harness.beforeEach?.(adapter);
|
|
37
|
+
});
|
|
38
|
+
describe("tryAcquire", () => {
|
|
39
|
+
it("first acquire wins on a free lock", async () => {
|
|
40
|
+
expect(await adapter.tryAcquire("cron.outbox", A, 5e3)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
it("second acquire by a different holder fails while the first is live", async () => {
|
|
43
|
+
await adapter.tryAcquire("cron.outbox", A, 5e3);
|
|
44
|
+
expect(await adapter.tryAcquire("cron.outbox", B, 5e3)).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
it("same holder may extend (idempotent re-acquire)", async () => {
|
|
47
|
+
expect(await adapter.tryAcquire("cron.outbox", A, 5e3)).toBe(true);
|
|
48
|
+
expect(await adapter.tryAcquire("cron.outbox", A, 5e3)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it("expired lease is reclaimable by another holder", async () => {
|
|
51
|
+
expect(await adapter.tryAcquire("cron.outbox", A, 1)).toBe(true);
|
|
52
|
+
await sleep(10);
|
|
53
|
+
expect(await adapter.tryAcquire("cron.outbox", B, 5e3)).toBe(true);
|
|
54
|
+
expect(await adapter.tryAcquire("cron.outbox", A, 5e3)).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
it("different lock names are independent", async () => {
|
|
57
|
+
expect(await adapter.tryAcquire("lock.one", A, 5e3)).toBe(true);
|
|
58
|
+
expect(await adapter.tryAcquire("lock.two", A, 5e3)).toBe(true);
|
|
59
|
+
expect(await adapter.tryAcquire("lock.one", B, 5e3)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
it("parallel acquires resolve to exactly one winner", async () => {
|
|
62
|
+
expect((await Promise.all([adapter.tryAcquire("shared.name", A, 5e3), adapter.tryAcquire("shared.name", B, 5e3)])).filter((r) => r === true)).toHaveLength(1);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
describe("release", () => {
|
|
66
|
+
it("the holder can release their own lock", async () => {
|
|
67
|
+
await adapter.tryAcquire("cron.outbox", A, 5e3);
|
|
68
|
+
expect(await adapter.release("cron.outbox", A)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
it("a non-holder cannot release", async () => {
|
|
71
|
+
await adapter.tryAcquire("cron.outbox", A, 5e3);
|
|
72
|
+
expect(await adapter.release("cron.outbox", B)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
it("release on an unheld lock returns false (idempotent)", async () => {
|
|
75
|
+
expect(await adapter.release("never.acquired", A)).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
it("after release, another holder can acquire", async () => {
|
|
78
|
+
await adapter.tryAcquire("cron.outbox", A, 5e3);
|
|
79
|
+
await adapter.release("cron.outbox", A);
|
|
80
|
+
expect(await adapter.tryAcquire("cron.outbox", B, 5e3)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it("repeated release by the same holder returns false on the second call", async () => {
|
|
83
|
+
await adapter.tryAcquire("cron.outbox", A, 5e3);
|
|
84
|
+
expect(await adapter.release("cron.outbox", A)).toBe(true);
|
|
85
|
+
expect(await adapter.release("cron.outbox", A)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe("inspect", () => {
|
|
89
|
+
it("reports the current holder for a live lock", async () => {
|
|
90
|
+
if (!adapter.inspect) return;
|
|
91
|
+
await adapter.tryAcquire("cron.outbox", A, 5e3);
|
|
92
|
+
const state = await adapter.inspect("cron.outbox");
|
|
93
|
+
expect(state).toBeTruthy();
|
|
94
|
+
expect(state?.name).toBe("cron.outbox");
|
|
95
|
+
expect(state?.holder).toBe(A);
|
|
96
|
+
expect(state?.expiresAt).toBeInstanceOf(Date);
|
|
97
|
+
expect(state?.acquiredAt).toBeInstanceOf(Date);
|
|
98
|
+
});
|
|
99
|
+
it("returns null for a never-acquired lock", async () => {
|
|
100
|
+
if (!adapter.inspect) return;
|
|
101
|
+
expect(await adapter.inspect("never.acquired")).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
it("returns null for an expired lock (treats expired as absent)", async () => {
|
|
104
|
+
if (!adapter.inspect) return;
|
|
105
|
+
await adapter.tryAcquire("cron.outbox", A, 1);
|
|
106
|
+
await sleep(10);
|
|
107
|
+
expect(await adapter.inspect("cron.outbox")).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
it("preserves acquiredAt across same-holder extensions", async () => {
|
|
110
|
+
if (!adapter.inspect) return;
|
|
111
|
+
await adapter.tryAcquire("cron.outbox", A, 5e3);
|
|
112
|
+
const original = (await adapter.inspect("cron.outbox"))?.acquiredAt;
|
|
113
|
+
await sleep(5);
|
|
114
|
+
await adapter.tryAcquire("cron.outbox", A, 5e3);
|
|
115
|
+
const extended = (await adapter.inspect("cron.outbox"))?.acquiredAt;
|
|
116
|
+
expect(extended?.getTime()).toBe(original?.getTime());
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe("post-steal semantics", () => {
|
|
120
|
+
it("the original holder cannot release after a steal", async () => {
|
|
121
|
+
await adapter.tryAcquire("cron.outbox", A, 1);
|
|
122
|
+
await sleep(10);
|
|
123
|
+
await adapter.tryAcquire("cron.outbox", B, 5e3);
|
|
124
|
+
expect(await adapter.release("cron.outbox", A)).toBe(false);
|
|
125
|
+
if (adapter.inspect) expect((await adapter.inspect("cron.outbox"))?.holder).toBe(B);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe("stress", () => {
|
|
129
|
+
it("50 concurrent holders against one name → exactly one winner", async () => {
|
|
130
|
+
const holders = Array.from({ length: 50 }, (_, i) => `replica-${i}`);
|
|
131
|
+
expect((await Promise.all(holders.map((h) => adapter.tryAcquire("contended", h, 5e3)))).filter((r) => r === true)).toHaveLength(1);
|
|
132
|
+
});
|
|
133
|
+
it("100 sequential acquire/release cycles leave no residue", async () => {
|
|
134
|
+
for (let i = 0; i < 100; i++) {
|
|
135
|
+
expect(await adapter.tryAcquire("cycled", A, 5e3)).toBe(true);
|
|
136
|
+
expect(await adapter.release("cycled", A)).toBe(true);
|
|
137
|
+
}
|
|
138
|
+
expect(await adapter.tryAcquire("cycled", B, 5e3)).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
it("100 same-holder extensions preserve the original acquiredAt", async () => {
|
|
141
|
+
if (!adapter.inspect) return;
|
|
142
|
+
await adapter.tryAcquire("extended", A, 5e3);
|
|
143
|
+
const original = (await adapter.inspect("extended"))?.acquiredAt;
|
|
144
|
+
expect(original).toBeTruthy();
|
|
145
|
+
for (let i = 0; i < 100; i++) await adapter.tryAcquire("extended", A, 5e3);
|
|
146
|
+
const final = (await adapter.inspect("extended"))?.acquiredAt;
|
|
147
|
+
expect(final?.getTime()).toBe(original?.getTime());
|
|
148
|
+
});
|
|
149
|
+
it("ownership churn: A → B → C → A handovers each succeed atomically", async () => {
|
|
150
|
+
for (const holder of [
|
|
151
|
+
A,
|
|
152
|
+
B,
|
|
153
|
+
"replica-C",
|
|
154
|
+
A
|
|
155
|
+
]) {
|
|
156
|
+
expect(await adapter.tryAcquire("churn", holder, 5e3)).toBe(true);
|
|
157
|
+
expect(await adapter.release("churn", holder)).toBe(true);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function sleep(ms) {
|
|
164
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
165
|
+
}
|
|
166
|
+
//#endregion
|
|
167
|
+
export { runLockAdapterConformance };
|
package/dist/testing/types.d.mts
CHANGED
|
@@ -26,6 +26,70 @@ interface ConformanceDoc {
|
|
|
26
26
|
notes: string | null;
|
|
27
27
|
createdAt: string;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Per-aggregate-op support matrix. Some aggregate ops aren't
|
|
31
|
+
* portable across every backend — `percentile` requires Mongo 7+'s
|
|
32
|
+
* `$percentile` accumulator or SQL's `PERCENTILE_CONT`, neither of
|
|
33
|
+
* which sqlitekit ships. Scenarios that exercise a non-universal
|
|
34
|
+
* op gate on the matching flag and `it.skip` on the off branch so
|
|
35
|
+
* the suite runs cleanly across every environment.
|
|
36
|
+
*
|
|
37
|
+
* **Stability contract.** Adding a flag here is additive — kits
|
|
38
|
+
* that don't declare the new key default to `false`, which is the
|
|
39
|
+
* conservative choice. Renaming or removing a flag is a breaking
|
|
40
|
+
* change.
|
|
41
|
+
*
|
|
42
|
+
* **Naming convention.** Flag names match the IR field they gate
|
|
43
|
+
* (`percentile` → `AggMeasure.op === 'percentile'`). When in doubt,
|
|
44
|
+
* grep the IR types and use the same identifier.
|
|
45
|
+
*/
|
|
46
|
+
interface AggregateOpsSupport {
|
|
47
|
+
/**
|
|
48
|
+
* `{ op: 'percentile', field, p }` measure. Mongokit (Mongo 7+)
|
|
49
|
+
* supports it; sqlitekit throws by design (no native function).
|
|
50
|
+
* Hosts targeting percentile dashboards pin to a kit that supports it.
|
|
51
|
+
*/
|
|
52
|
+
percentile?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* `{ op: 'stddev', field }` / `{ op: 'stddevPop', field }` measures.
|
|
55
|
+
* Mongokit supports both via native `$stdDevSamp` / `$stdDevPop`
|
|
56
|
+
* (Welford). Sqlitekit throws — SQLite has no native STDDEV and
|
|
57
|
+
* the computational formula is numerically unstable. Hosts pin
|
|
58
|
+
* to mongokit / future pgkit when stddev is load-bearing.
|
|
59
|
+
*/
|
|
60
|
+
stddev?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* `topN: { partitionBy, sortBy, limit, ties }` filter. Both
|
|
63
|
+
* mongokit and sqlitekit support it as of repo-core 0.4.x; the
|
|
64
|
+
* flag exists for future kits that may not ship window-function
|
|
65
|
+
* equivalents.
|
|
66
|
+
*/
|
|
67
|
+
topN?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* `dateBuckets: { ..., interval: { every, unit } }` custom-bin
|
|
70
|
+
* form. Kits that only support named-bucket form can leave this
|
|
71
|
+
* `false`; tests for `'minute'` / `'hour'` named intervals are
|
|
72
|
+
* gated separately via `dateBucketSubMinute`.
|
|
73
|
+
*/
|
|
74
|
+
customDateBuckets?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Sub-day-granularity named buckets (`'minute'` / `'hour'`).
|
|
77
|
+
* Older kits may only support day+ named intervals; flag exists
|
|
78
|
+
* to gate those scenarios cleanly.
|
|
79
|
+
*/
|
|
80
|
+
dateBucketSubMinute?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Per-request `cache?: AggCacheOptions` slot — TTL / tags / SWR /
|
|
83
|
+
* bypass / `repo.invalidateAggregateCache(tags)`. Both mongokit
|
|
84
|
+
* and sqlitekit support it as of repo-core 0.4.x. Future kits
|
|
85
|
+
* without the wiring can leave this false to skip cache scenarios.
|
|
86
|
+
*
|
|
87
|
+
* Independent of which CACHE BACKEND the harness wires — test
|
|
88
|
+
* scenarios construct their own `createMemoryCacheAdapter()` so
|
|
89
|
+
* this flag is purely "does the kit honour the request slot".
|
|
90
|
+
*/
|
|
91
|
+
cache?: boolean;
|
|
92
|
+
}
|
|
29
93
|
/**
|
|
30
94
|
* Per-backend feature flags. Scenarios that exercise a non-universal
|
|
31
95
|
* capability (transactions in D1, upsert in narrow stores) check the
|
|
@@ -48,8 +112,22 @@ interface ConformanceFeatures {
|
|
|
48
112
|
duplicateKeyError: boolean;
|
|
49
113
|
/** `distinct(field)`. */
|
|
50
114
|
distinct: boolean;
|
|
51
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Portable `aggregate({ measures, groupBy, having })`. Coarse
|
|
117
|
+
* top-level flag — gates the entire `describe('aggregate')` block.
|
|
118
|
+
* Per-op flags live on `aggregateOps` for asymmetric capabilities
|
|
119
|
+
* (percentile, custom date bins, etc.) that some kits skip while
|
|
120
|
+
* still supporting the core aggregate surface.
|
|
121
|
+
*/
|
|
52
122
|
aggregate: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* Per-op feature matrix for the aggregate surface. Optional —
|
|
125
|
+
* absent matrix or absent key both mean "not supported", so kits
|
|
126
|
+
* opt INTO scenarios for ops they implement. This avoids the
|
|
127
|
+
* trap where a future kit silently fails percentile tests because
|
|
128
|
+
* it forgot to set the flag.
|
|
129
|
+
*/
|
|
130
|
+
aggregateOps?: AggregateOpsSupport;
|
|
53
131
|
/** `getOrCreate(filter, data)`. */
|
|
54
132
|
getOrCreate: boolean;
|
|
55
133
|
/** `count(filter)` and `exists(filter)`. */
|
|
@@ -69,6 +147,25 @@ interface ConformanceContext<TDoc extends ConformanceDoc = ConformanceDoc> {
|
|
|
69
147
|
* be omitted; scenarios that need it will skip if missing.
|
|
70
148
|
*/
|
|
71
149
|
secondaryRepo?: StandardRepo<TDoc> & MinimalRepo<TDoc>;
|
|
150
|
+
/**
|
|
151
|
+
* Optional repo-with-cache for cache-suite scenarios. Required iff
|
|
152
|
+
* the harness declares `features.aggregateOps.cache === true`.
|
|
153
|
+
*
|
|
154
|
+
* **Why a separate repo** rather than reconfiguring the primary
|
|
155
|
+
* one: cache state survives across calls within a test, and the
|
|
156
|
+
* primary repo is shared by every other suite. A dedicated cached
|
|
157
|
+
* repo keeps the cache scenarios hermetic.
|
|
158
|
+
*
|
|
159
|
+
* The bound type widens to include `invalidateAggregateCache` —
|
|
160
|
+
* cast at the use site since the contract type
|
|
161
|
+
* (`StandardRepo<TDoc>`) doesn't currently surface it. This
|
|
162
|
+
* intentionally leaves the contract narrower than the
|
|
163
|
+
* implementation; consumers pin to a specific kit when they need
|
|
164
|
+
* the invalidate method on their `RepositoryLike` slot.
|
|
165
|
+
*/
|
|
166
|
+
cachedRepo?: StandardRepo<TDoc> & MinimalRepo<TDoc> & {
|
|
167
|
+
invalidateAggregateCache(tags?: readonly string[]): Promise<number>;
|
|
168
|
+
};
|
|
72
169
|
/** Release resources (close db, drop collection). Must be idempotent. */
|
|
73
170
|
cleanup(): Promise<void>;
|
|
74
171
|
}
|
|
@@ -110,4 +207,4 @@ interface ConformanceHarness<TDoc extends ConformanceDoc = ConformanceDoc> {
|
|
|
110
207
|
makeDoc(overrides?: Partial<ConformanceDoc>): Partial<TDoc>;
|
|
111
208
|
}
|
|
112
209
|
//#endregion
|
|
113
|
-
export { ConformanceContext, ConformanceDoc, ConformanceFeatures, ConformanceHarness };
|
|
210
|
+
export { AggregateOpsSupport, ConformanceContext, ConformanceDoc, ConformanceFeatures, ConformanceHarness };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/repo-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Driver-agnostic repository primitives: hooks, Filter IR, operations, pagination, cache contract. Foundation for mongokit, sqlitekit, pgkit, and prismakit. Lean by design — no plugins ship here; each kit owns its own.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -70,6 +70,26 @@
|
|
|
70
70
|
"types": "./dist/lookup/index.d.mts",
|
|
71
71
|
"default": "./dist/lookup/index.mjs"
|
|
72
72
|
},
|
|
73
|
+
"./adapter": {
|
|
74
|
+
"types": "./dist/adapter/index.d.mts",
|
|
75
|
+
"default": "./dist/adapter/index.mjs"
|
|
76
|
+
},
|
|
77
|
+
"./better-auth": {
|
|
78
|
+
"types": "./dist/better-auth/index.d.mts",
|
|
79
|
+
"default": "./dist/better-auth/index.mjs"
|
|
80
|
+
},
|
|
81
|
+
"./aggregate": {
|
|
82
|
+
"types": "./dist/aggregate/index.d.mts",
|
|
83
|
+
"default": "./dist/aggregate/index.mjs"
|
|
84
|
+
},
|
|
85
|
+
"./plugins": {
|
|
86
|
+
"types": "./dist/plugins/index.d.mts",
|
|
87
|
+
"default": "./dist/plugins/index.mjs"
|
|
88
|
+
},
|
|
89
|
+
"./lock": {
|
|
90
|
+
"types": "./dist/lock/index.d.mts",
|
|
91
|
+
"default": "./dist/lock/index.mjs"
|
|
92
|
+
},
|
|
73
93
|
"./package.json": "./package.json"
|
|
74
94
|
},
|
|
75
95
|
"keywords": [
|
|
@@ -105,6 +125,7 @@
|
|
|
105
125
|
"test:e2e": "vitest run --project e2e",
|
|
106
126
|
"test:all": "vitest run",
|
|
107
127
|
"test:watch": "vitest --project unit --project integration",
|
|
128
|
+
"bench": "vitest bench --run --project bench",
|
|
108
129
|
"test:coverage": "vitest run --coverage",
|
|
109
130
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
110
131
|
"lint": "biome check src tests",
|
|
@@ -125,6 +146,7 @@
|
|
|
125
146
|
"@classytic/dev-tools": "^0.2.0",
|
|
126
147
|
"@types/node": "^22.0.0",
|
|
127
148
|
"@vitest/coverage-v8": "^4.1.4",
|
|
149
|
+
"fast-check": "^4.7.0",
|
|
128
150
|
"knip": "^6.3.0",
|
|
129
151
|
"publint": "^0.3.18",
|
|
130
152
|
"tsdown": "^0.21.8",
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
//#region src/cache/stable-stringify.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Deterministic JSON stringify — equivalent values produce identical output.
|
|
4
|
-
*
|
|
5
|
-
* Used by kits that build cache keys from hook contexts so
|
|
6
|
-
* `{ b: 1, a: 2 }` and `{ a: 2, b: 1 }` hash to the same bucket. Arrays
|
|
7
|
-
* preserve order (order is part of array identity).
|
|
8
|
-
*
|
|
9
|
-
* Extracted from the mongokit / sqlitekit cache plugins into repo-core so
|
|
10
|
-
* every kit's cachePlugin uses the same keying rule — cross-kit caches
|
|
11
|
-
* remain bucket-compatible.
|
|
12
|
-
*/
|
|
13
|
-
declare function stableStringify(value: unknown): string;
|
|
14
|
-
//#endregion
|
|
15
|
-
export { stableStringify };
|