@classytic/repo-core 0.5.0 → 0.6.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.
@@ -0,0 +1,59 @@
1
+ import { ERROR_CODES } from "../errors/types.mjs";
2
+ //#region src/schema/standard-schema.ts
3
+ /**
4
+ * Standard Schema integration — the validator-agnostic validation slot.
5
+ *
6
+ * [Standard Schema](https://standardschema.dev) is the shared interface
7
+ * implemented by Zod 3.24+, Valibot 1.0+, ArkType 2.0+, Effect Schema and
8
+ * others. Vendoring the interface (officially encouraged — it's a
9
+ * types-only spec designed to be copied) keeps repo-core's zero-dependency
10
+ * guarantee while letting hosts plug ANY conforming validator into a
11
+ * repository:
12
+ *
13
+ * ```ts
14
+ * import { z } from 'zod';
15
+ *
16
+ * const repo = createRepository(UserModel, {
17
+ * schema: z.object({ name: z.string(), email: z.string().email() }),
18
+ * });
19
+ * await repo.create({ name: 1 }); // throws HttpError 400 with validationErrors
20
+ * ```
21
+ *
22
+ * `RepositoryBase` wires `schema` / `updateSchema` into `before:create` /
23
+ * `before:createMany` / `before:update` hooks at `HOOK_PRIORITY.VALIDATION`
24
+ * — after policy plugins (so tenant-stamped fields are present) and before
25
+ * cache/observability.
26
+ */
27
+ /** Dot-path string from a Standard Schema issue path. */
28
+ function issuePath(issue) {
29
+ if (!issue.path || issue.path.length === 0) return "";
30
+ return issue.path.map((seg) => String(typeof seg === "object" && seg !== null && "key" in seg ? seg.key : seg)).join(".");
31
+ }
32
+ /**
33
+ * Validate `data` against a Standard Schema. Returns the schema's typed
34
+ * output (validators may coerce/transform) or throws an `HttpError` 400
35
+ * carrying `validationErrors` + structured `meta.issues` — the same wire
36
+ * shape every kit's own validation errors serialize to.
37
+ */
38
+ async function validateStandardSchema(schema, data) {
39
+ let result = schema["~standard"].validate(data);
40
+ if (result instanceof Promise) result = await result;
41
+ if (result.issues) {
42
+ const validationErrors = result.issues.map((issue) => ({
43
+ validator: schema["~standard"].vendor,
44
+ error: issuePath(issue) ? `${issuePath(issue)}: ${issue.message}` : issue.message
45
+ }));
46
+ throw Object.assign(/* @__PURE__ */ new Error("Validation failed"), {
47
+ status: 400,
48
+ code: ERROR_CODES.VALIDATION,
49
+ validationErrors,
50
+ meta: { issues: result.issues.map((issue) => ({
51
+ path: issuePath(issue) || void 0,
52
+ message: issue.message
53
+ })) }
54
+ });
55
+ }
56
+ return result.value;
57
+ }
58
+ //#endregion
59
+ export { validateStandardSchema };
@@ -1,4 +1,5 @@
1
- import { AggregateOpsSupport, ConformanceContext, ConformanceDoc, ConformanceFeatures, ConformanceHarness } from "./types.mjs";
1
+ import { AggregateOpsSupport } from "../repository/capabilities.mjs";
2
+ import { ConformanceContext, ConformanceDoc, ConformanceFeatures, ConformanceHarness } from "./types.mjs";
2
3
  import { runStandardRepoConformance } from "./conformance.mjs";
3
4
  import { LockConformanceHarness, runLockAdapterConformance } from "./lock-conformance.mjs";
4
5
  export { type AggregateOpsSupport, type ConformanceContext, type ConformanceDoc, type ConformanceFeatures, type ConformanceHarness, type LockConformanceHarness, runLockAdapterConformance, runStandardRepoConformance };
@@ -1,3 +1,4 @@
1
+ import { AggregateOpsSupport, RepoCapabilities } from "../repository/capabilities.mjs";
1
2
  import { MinimalRepo, StandardRepo } from "../repository/types.mjs";
2
3
 
3
4
  //#region src/testing/types.d.ts
@@ -27,119 +28,18 @@ interface ConformanceDoc {
27
28
  createdAt: string;
28
29
  }
29
30
  /**
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.
31
+ * Per-backend feature flags an alias of the runtime
32
+ * {@link RepoCapabilities} descriptor (one shape, no drift). Scenarios
33
+ * that exercise a non-universal capability (transactions in D1, upsert
34
+ * in narrow stores) check the flag and `it.skip` on the off branch — so
35
+ * the suite runs on every environment without "optional test failed"
36
+ * noise.
36
37
  *
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
- }
93
- /**
94
- * Per-backend feature flags. Scenarios that exercise a non-universal
95
- * capability (transactions in D1, upsert in narrow stores) check the
96
- * flag and `it.skip` on the off branch — so the suite runs on every
97
- * environment without "optional test failed" noise.
38
+ * **Single source of truth.** Kits declare `repo.capabilities` at
39
+ * runtime and pass the SAME object as the harness's `features` what a
40
+ * kit claims to support is exactly what the conformance suite verifies.
98
41
  */
99
- interface ConformanceFeatures {
100
- /** `withTransaction(fn)` — D1 throws, standalone Mongo throws 263. */
101
- transactions: boolean;
102
- /**
103
- * True if calling `withTransaction` inside another `withTransaction`
104
- * callback is expected to work. Mongo's driver supports it via the
105
- * same session; SQL drivers typically reject it. Either behavior is
106
- * valid — the scenario asserts whichever the harness declares.
107
- */
108
- nestedTransactions: boolean;
109
- /** `findOneAndUpdate` with upsert: true. */
110
- upsert: boolean;
111
- /** `isDuplicateKeyError(err)` classifier. */
112
- duplicateKeyError: boolean;
113
- /** `distinct(field)`. */
114
- distinct: boolean;
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
- */
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;
131
- /** `getOrCreate(filter, data)`. */
132
- getOrCreate: boolean;
133
- /** `count(filter)` and `exists(filter)`. */
134
- countAndExists: boolean;
135
- /**
136
- * `purgeByField(field, value, strategy, options)` — compliance-grade
137
- * tenant cleanup primitive. Both mongokit and sqlitekit ship this as
138
- * of repo-core 0.x. Future kits without it leave the flag absent
139
- * (defaults to false) and skip the cleanup scenarios.
140
- */
141
- purgeByField?: boolean;
142
- }
42
+ type ConformanceFeatures = RepoCapabilities;
143
43
  /**
144
44
  * One-shot context produced by `harness.setup()`. Scenarios receive a
145
45
  * fresh context per test — the harness is responsible for isolation
@@ -214,4 +114,4 @@ interface ConformanceHarness<TDoc extends ConformanceDoc = ConformanceDoc> {
214
114
  makeDoc(overrides?: Partial<ConformanceDoc>): Partial<TDoc>;
215
115
  }
216
116
  //#endregion
217
- export { AggregateOpsSupport, ConformanceContext, ConformanceDoc, ConformanceFeatures, ConformanceHarness };
117
+ export { ConformanceContext, ConformanceDoc, ConformanceFeatures, ConformanceHarness };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/repo-core",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
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,
@@ -54,6 +54,10 @@
54
54
  "types": "./dist/cache/index.d.mts",
55
55
  "default": "./dist/cache/index.mjs"
56
56
  },
57
+ "./events": {
58
+ "types": "./dist/events/index.d.mts",
59
+ "default": "./dist/events/index.mjs"
60
+ },
57
61
  "./schema": {
58
62
  "types": "./dist/schema/index.d.mts",
59
63
  "default": "./dist/schema/index.mjs"