@classytic/repo-core 0.31.0 → 0.32.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.
Files changed (205) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/_virtual/_rolldown/runtime.mjs +1 -2
  3. package/dist/adapter/index.mjs +1 -2
  4. package/dist/adapter/types.d.mts +0 -2
  5. package/dist/adapter/widen.d.mts +0 -2
  6. package/dist/adapter/widen.mjs +1 -2
  7. package/dist/aggregate/keyset.d.mts +0 -2
  8. package/dist/aggregate/keyset.mjs +1 -2
  9. package/dist/aggregate/normalize.d.mts +0 -2
  10. package/dist/aggregate/normalize.mjs +1 -2
  11. package/dist/better-auth/index.d.mts +0 -2
  12. package/dist/better-auth/index.mjs +1 -2
  13. package/dist/cache/engine.d.mts +0 -2
  14. package/dist/cache/engine.mjs +1 -2
  15. package/dist/cache/envelope.mjs +1 -2
  16. package/dist/cache/keys.mjs +1 -2
  17. package/dist/cache/memo.d.mts +0 -2
  18. package/dist/cache/memo.mjs +1 -2
  19. package/dist/cache/memory-adapter.d.mts +0 -2
  20. package/dist/cache/memory-adapter.mjs +1 -2
  21. package/dist/cache/options.d.mts +0 -2
  22. package/dist/cache/options.mjs +1 -2
  23. package/dist/cache/plugin/context.d.mts +0 -2
  24. package/dist/cache/plugin/context.mjs +1 -2
  25. package/dist/cache/plugin/index.d.mts +0 -2
  26. package/dist/cache/plugin/index.mjs +1 -2
  27. package/dist/cache/plugin/invalidation-hooks.mjs +1 -2
  28. package/dist/cache/plugin/read-hooks.mjs +1 -2
  29. package/dist/cache/plugin/swr.mjs +1 -2
  30. package/dist/cache/runtime.d.mts +0 -2
  31. package/dist/cache/runtime.mjs +0 -1
  32. package/dist/cache/stable-stringify.d.mts +0 -2
  33. package/dist/cache/stable-stringify.mjs +1 -2
  34. package/dist/cache/tag-index.mjs +1 -2
  35. package/dist/cache/timeout-adapter.d.mts +0 -2
  36. package/dist/cache/timeout-adapter.mjs +1 -2
  37. package/dist/cache/types.d.mts +0 -2
  38. package/dist/cache/version-store.mjs +1 -2
  39. package/dist/cleanup/define-purge-step.d.mts +0 -2
  40. package/dist/cleanup/define-purge-step.mjs +1 -2
  41. package/dist/cleanup/index.mjs +1 -2
  42. package/dist/cleanup/types.d.mts +0 -2
  43. package/dist/context/types.d.mts +0 -2
  44. package/dist/errors/codes.d.mts +0 -2
  45. package/dist/errors/codes.mjs +1 -2
  46. package/dist/errors/conflict.d.mts +0 -2
  47. package/dist/errors/conflict.mjs +1 -2
  48. package/dist/errors/contract.d.mts +0 -2
  49. package/dist/errors/contract.mjs +1 -2
  50. package/dist/errors/create-error.d.mts +0 -2
  51. package/dist/errors/create-error.mjs +1 -2
  52. package/dist/errors/duplicate-key.d.mts +0 -2
  53. package/dist/errors/duplicate-key.mjs +1 -2
  54. package/dist/errors/schema.d.mts +0 -2
  55. package/dist/errors/schema.mjs +1 -2
  56. package/dist/errors/types.d.mts +0 -2
  57. package/dist/events/emit.d.mts +0 -2
  58. package/dist/events/emit.mjs +1 -2
  59. package/dist/events/index.mjs +1 -2
  60. package/dist/events/types.d.mts +0 -2
  61. package/dist/filter/builders.d.mts +0 -2
  62. package/dist/filter/builders.mjs +1 -2
  63. package/dist/filter/coerce-dates.d.mts +0 -2
  64. package/dist/filter/coerce-dates.mjs +1 -2
  65. package/dist/filter/from-record.d.mts +0 -2
  66. package/dist/filter/from-record.mjs +1 -2
  67. package/dist/filter/guard.d.mts +0 -2
  68. package/dist/filter/guard.mjs +1 -2
  69. package/dist/filter/match-record.d.mts +0 -2
  70. package/dist/filter/match-record.mjs +1 -2
  71. package/dist/filter/match.d.mts +0 -2
  72. package/dist/filter/match.mjs +1 -2
  73. package/dist/filter/scope.d.mts +0 -2
  74. package/dist/filter/scope.mjs +1 -2
  75. package/dist/filter/types.d.mts +0 -2
  76. package/dist/filter/walk.d.mts +0 -2
  77. package/dist/filter/walk.mjs +1 -2
  78. package/dist/hash/index.d.mts +0 -2
  79. package/dist/hash/index.mjs +1 -2
  80. package/dist/hooks/engine.d.mts +0 -2
  81. package/dist/hooks/engine.mjs +1 -2
  82. package/dist/hooks/events.d.mts +0 -2
  83. package/dist/hooks/events.mjs +1 -2
  84. package/dist/hooks/priority.d.mts +0 -2
  85. package/dist/hooks/priority.mjs +1 -2
  86. package/dist/hooks/types.d.mts +0 -2
  87. package/dist/idempotency/index.d.mts +0 -2
  88. package/dist/idempotency/index.mjs +1 -2
  89. package/dist/loader/batch-loader.d.mts +0 -2
  90. package/dist/loader/batch-loader.mjs +1 -2
  91. package/dist/loader/index.mjs +1 -2
  92. package/dist/lock/index.d.mts +0 -2
  93. package/dist/lock/index.mjs +1 -2
  94. package/dist/lookup/types.d.mts +0 -2
  95. package/dist/operations/index.mjs +1 -2
  96. package/dist/operations/registry.d.mts +0 -2
  97. package/dist/operations/registry.mjs +1 -2
  98. package/dist/operations/types.d.mts +0 -2
  99. package/dist/pagination/canonical.d.mts +0 -2
  100. package/dist/pagination/canonical.mjs +1 -2
  101. package/dist/pagination/cursor.d.mts +0 -2
  102. package/dist/pagination/cursor.mjs +1 -2
  103. package/dist/pagination/keyset.d.mts +0 -2
  104. package/dist/pagination/keyset.mjs +1 -2
  105. package/dist/pagination/merge-keyset.d.mts +0 -2
  106. package/dist/pagination/merge-keyset.mjs +1 -2
  107. package/dist/pagination/offset.d.mts +0 -2
  108. package/dist/pagination/offset.mjs +1 -2
  109. package/dist/pagination/types.d.mts +0 -2
  110. package/dist/plugins/index.mjs +1 -2
  111. package/dist/plugins/tenant-helpers.d.mts +0 -2
  112. package/dist/plugins/tenant-helpers.mjs +1 -2
  113. package/dist/query-parser/coerce.d.mts +0 -2
  114. package/dist/query-parser/coerce.mjs +1 -2
  115. package/dist/query-parser/errors.d.mts +0 -2
  116. package/dist/query-parser/errors.mjs +1 -2
  117. package/dist/query-parser/format-query.d.mts +30 -0
  118. package/dist/query-parser/format-query.mjs +90 -0
  119. package/dist/query-parser/grammar.d.mts +0 -2
  120. package/dist/query-parser/grammar.mjs +1 -2
  121. package/dist/query-parser/index.d.mts +2 -1
  122. package/dist/query-parser/index.mjs +2 -1
  123. package/dist/query-parser/parse-url.d.mts +0 -2
  124. package/dist/query-parser/parse-url.mjs +1 -2
  125. package/dist/query-parser/regex-safety.d.mts +0 -2
  126. package/dist/query-parser/regex-safety.mjs +1 -2
  127. package/dist/query-parser/reserved.d.mts +0 -2
  128. package/dist/query-parser/reserved.mjs +1 -2
  129. package/dist/query-parser/types.d.mts +0 -2
  130. package/dist/repository/agg-output.d.mts +0 -2
  131. package/dist/repository/agg-output.mjs +1 -2
  132. package/dist/repository/archive.d.mts +0 -2
  133. package/dist/repository/archive.mjs +1 -2
  134. package/dist/repository/base.d.mts +0 -2
  135. package/dist/repository/base.mjs +1 -2
  136. package/dist/repository/bulk-upsert.d.mts +0 -2
  137. package/dist/repository/bulk-upsert.mjs +1 -2
  138. package/dist/repository/capabilities.d.mts +0 -2
  139. package/dist/repository/distribution.d.mts +0 -2
  140. package/dist/repository/distribution.mjs +1 -2
  141. package/dist/repository/options.d.mts +0 -2
  142. package/dist/repository/options.mjs +1 -2
  143. package/dist/repository/plugin-types.d.mts +0 -2
  144. package/dist/repository/plugin-types.mjs +1 -2
  145. package/dist/repository/purge.d.mts +0 -2
  146. package/dist/repository/purge.mjs +1 -2
  147. package/dist/repository/read-only.d.mts +0 -2
  148. package/dist/repository/read-only.mjs +1 -2
  149. package/dist/repository/resilience.d.mts +0 -2
  150. package/dist/repository/resilience.mjs +1 -2
  151. package/dist/repository/retrying-transaction.d.mts +0 -2
  152. package/dist/repository/retrying-transaction.mjs +1 -2
  153. package/dist/repository/types.d.mts +0 -2
  154. package/dist/schema/field-rules.d.mts +0 -2
  155. package/dist/schema/field-rules.mjs +1 -2
  156. package/dist/schema/generator.d.mts +0 -2
  157. package/dist/schema/generator.mjs +1 -2
  158. package/dist/schema/standard-schema.d.mts +0 -2
  159. package/dist/schema/standard-schema.mjs +1 -2
  160. package/dist/schema/types.d.mts +0 -2
  161. package/dist/sync/index.d.mts +0 -2
  162. package/dist/sync/index.mjs +0 -0
  163. package/dist/tenant/index.mjs +1 -2
  164. package/dist/tenant/resolve.d.mts +0 -2
  165. package/dist/tenant/resolve.mjs +1 -2
  166. package/dist/tenant/types.d.mts +0 -2
  167. package/dist/testing/change-log-conformance.d.mts +0 -2
  168. package/dist/testing/change-log-conformance.mjs +1 -2
  169. package/dist/testing/command-stream-conformance.d.mts +0 -2
  170. package/dist/testing/command-stream-conformance.mjs +1 -2
  171. package/dist/testing/conformance.d.mts +0 -2
  172. package/dist/testing/conformance.mjs +1 -2
  173. package/dist/testing/data-access-conformance.d.mts +0 -2
  174. package/dist/testing/data-access-conformance.mjs +1 -2
  175. package/dist/testing/idempotency-conformance.d.mts +0 -2
  176. package/dist/testing/idempotency-conformance.mjs +1 -2
  177. package/dist/testing/lock-conformance.d.mts +0 -2
  178. package/dist/testing/lock-conformance.mjs +1 -2
  179. package/dist/testing/merge-keyset-conformance.d.mts +0 -2
  180. package/dist/testing/merge-keyset-conformance.mjs +1 -2
  181. package/dist/testing/pagination-property-conformance.d.mts +0 -2
  182. package/dist/testing/pagination-property-conformance.mjs +1 -2
  183. package/dist/testing/purge-conformance.d.mts +0 -2
  184. package/dist/testing/purge-conformance.mjs +1 -2
  185. package/dist/testing/query-grammar-conformance.d.mts +0 -2
  186. package/dist/testing/query-grammar-conformance.mjs +1 -2
  187. package/dist/testing/read-model-property-conformance.d.mts +0 -2
  188. package/dist/testing/read-model-property-conformance.mjs +1 -2
  189. package/dist/testing/types.d.mts +0 -2
  190. package/dist/testing/usage-conformance.d.mts +0 -2
  191. package/dist/testing/usage-conformance.mjs +1 -2
  192. package/dist/update/builders.d.mts +0 -2
  193. package/dist/update/builders.mjs +1 -2
  194. package/dist/update/compile.d.mts +0 -2
  195. package/dist/update/compile.mjs +1 -2
  196. package/dist/update/guard.d.mts +0 -2
  197. package/dist/update/guard.mjs +1 -2
  198. package/dist/update/types.d.mts +0 -2
  199. package/dist/usage/index.d.mts +0 -2
  200. package/dist/usage/index.mjs +1 -2
  201. package/llms.txt +29 -0
  202. package/package.json +16 -5
  203. package/skills/repo-core/SKILL.md +108 -0
  204. package/skills/repo-core/references/contracts.md +69 -0
  205. package/skills/repo-core/references/kit-authoring.md +75 -0
@@ -1,5 +1,4 @@
1
1
  import { UpdateSpec } from "./types.mjs";
2
- //#region src/update/compile.d.ts
3
2
  /**
4
3
  * Compile an `UpdateSpec` to a Mongo operator record.
5
4
  *
@@ -41,5 +40,4 @@ interface SqlUpdatePlan {
41
40
  readonly insertDefaults: Readonly<Record<string, unknown>>;
42
41
  }
43
42
  declare function compileUpdateSpecToSql(spec: UpdateSpec): SqlUpdatePlan;
44
- //#endregion
45
43
  export { SqlUpdatePlan, compileUpdateSpecToMongo, compileUpdateSpecToSql };
@@ -1,4 +1,4 @@
1
- //#region src/update/compile.ts
1
+ // src/update/compile.ts
2
2
  /**
3
3
  * Compile an `UpdateSpec` to a Mongo operator record.
4
4
  *
@@ -29,5 +29,4 @@ function compileUpdateSpecToSql(spec) {
29
29
  insertDefaults: Object.freeze({ ...spec.setOnInsert ?? {} })
30
30
  });
31
31
  }
32
- //#endregion
33
32
  export { compileUpdateSpecToMongo, compileUpdateSpecToSql };
@@ -1,5 +1,4 @@
1
1
  import { UpdateSpec } from "./types.mjs";
2
- //#region src/update/guard.d.ts
3
2
  /**
4
3
  * True when `value` is an `UpdateSpec` — i.e. the portable, compile-to-native
5
4
  * form.
@@ -15,5 +14,4 @@ declare function isUpdateSpec(value: unknown): value is UpdateSpec;
15
14
  * pipelines.
16
15
  */
17
16
  declare function isUpdatePipeline(value: unknown): value is Record<string, unknown>[];
18
- //#endregion
19
17
  export { isUpdatePipeline, isUpdateSpec };
@@ -1,4 +1,4 @@
1
- //#region src/update/guard.ts
1
+ // src/update/guard.ts
2
2
  /**
3
3
  * True when `value` is an `UpdateSpec` — i.e. the portable, compile-to-native
4
4
  * form.
@@ -20,5 +20,4 @@ function isUpdateSpec(value) {
20
20
  function isUpdatePipeline(value) {
21
21
  return Array.isArray(value);
22
22
  }
23
- //#endregion
24
23
  export { isUpdatePipeline, isUpdateSpec };
@@ -1,4 +1,3 @@
1
- //#region src/update/types.d.ts
2
1
  /**
3
2
  * Update IR — driver-agnostic mutation spec.
4
3
  *
@@ -58,5 +57,4 @@ interface UpdateSpec {
58
57
  * escape hatch (e.g. outbox's `$ifNull` to preserve `firstFailedAt`).
59
58
  */
60
59
  type UpdateInput = UpdateSpec | Record<string, unknown> | Record<string, unknown>[];
61
- //#endregion
62
60
  export { UpdateInput, UpdateSpec };
@@ -1,4 +1,3 @@
1
- //#region src/usage/index.d.ts
2
1
  /**
3
2
  * Usage-counter contract for the @classytic ecosystem.
4
3
  *
@@ -73,5 +72,4 @@ declare function usagePeriod(date?: Date): string;
73
72
  declare function createMemoryUsageStore(): UsageStore & {
74
73
  clear(): void;
75
74
  };
76
- //#endregion
77
75
  export { UsageBucket, UsageStore, createMemoryUsageStore, usagePeriod };
@@ -1,4 +1,4 @@
1
- //#region src/usage/index.ts
1
+ // src/usage/index.ts
2
2
  /**
3
3
  * Canonical period key for a date — calendar month, UTC: `2026-07`.
4
4
  * Monthly is the billing-native granularity.
@@ -32,5 +32,4 @@ function createMemoryUsageStore() {
32
32
  }
33
33
  };
34
34
  }
35
- //#endregion
36
35
  export { createMemoryUsageStore, usagePeriod };
package/llms.txt ADDED
@@ -0,0 +1,29 @@
1
+ # @classytic/repo-core
2
+
3
+ Contract layer under mongokit, sqlitekit and arc. No root import: use subpaths (@classytic/repo-core/pagination, /errors, ...). Skill: skills/repo-core/SKILL.md.
4
+
5
+ | Need | Use |
6
+ |---|---|
7
+ | List response shape on the wire | `toCanonicalList` from `@classytic/repo-core/pagination` |
8
+ | Page result types | `OffsetPaginationResult`, `KeysetPaginationResult`, `PaginatedResult` |
9
+ | Error JSON body | `toErrorContract`, `ErrorContract` from `@classytic/repo-core/errors` |
10
+ | Throw an HTTP error | `createError`, `isHttpError` |
11
+ | Error code vocabulary | `ERROR_CODES`, `ARC_ERROR_CODES`, `CURSOR_ERROR_CODES` |
12
+ | Duplicate key to 409 | `toDuplicateKeyHttpError` |
13
+ | Query string to filter, sort, page | `parseUrl` from `@classytic/repo-core/query-parser` |
14
+ | Filters and sort to URL params (client side) | `formatQuery`, `stringifyQuery` from `@classytic/repo-core/query-parser` |
15
+ | Portable filters | `and`, `eq`, `in_`, `between` from `@classytic/repo-core/filter` |
16
+ | Merge tenant scope into a filter | `buildTenantScope`, `mergeScope` |
17
+ | Portable update spec | `update`, `incFields` from `@classytic/repo-core/update` |
18
+ | Keyset cursors | `encodeCursor`, `decodeCursor`, `mergeKeysetPages` |
19
+ | Run once per idempotency key | `runIdempotent`, `fingerprintRequest` |
20
+ | Retry transient errors | `withRetry`, `retryingTransaction` |
21
+ | Batch lookups (N+1) | `createBatchLoader` |
22
+ | Read-only repository | `asReadOnlyRepo` |
23
+ | Hook priorities and event names | `HOOK_PRIORITY`, `HOOK_EVENTS` |
24
+ | Cache adapter contract | `CacheAdapter`, `createMemoryCacheAdapter` |
25
+ | Purge step for tenant cleanup | `definePurgeStep` |
26
+ | Cross-kit conformance tests | `runStandardRepoConformance` from `@classytic/repo-core/testing` |
27
+
28
+ Details: references/contracts.md (envelopes, errors, grammar), references/kit-authoring.md (kits, plugins, conformance).
29
+ Mongo implementation: skill mongokit.
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@classytic/repo-core",
3
- "version": "0.31.0",
4
- "description": "Driver-agnostic repository primitives: hooks, Filter IR, operations, pagination, cache contract. Foundation for mongokit, sqlitekit, pgkit, and prismakit. Lean by design \u2014 no plugins ship here; each kit owns its own.",
3
+ "version": "0.32.0",
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,
7
7
  "files": [
8
8
  "dist",
9
+ "skills",
10
+ "llms.txt",
9
11
  "README.md",
10
12
  "LICENSE",
11
13
  "CHANGELOG.md"
@@ -161,20 +163,29 @@
161
163
  "format": "biome format src tests --write",
162
164
  "check": "biome ci src tests --diagnostic-level=error",
163
165
  "knip": "knip",
166
+ "check:skills": "node scripts/check-skills.mjs",
164
167
  "push": "classytic-push",
165
- "prepublishOnly": "npm run check && npm run build && npm run typecheck && npm test",
168
+ "prepublishOnly": "npm run check && npm run build && npm run check:skills && npm run typecheck && npm test",
166
169
  "release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
167
170
  "release": "npm run push -- main && npm run release:tag && npm publish",
168
171
  "publish:dry": "npm publish --dry-run --access public",
169
172
  "publish:npm": "npm publish --access public"
170
173
  },
174
+ "peerDependencies": {
175
+ "fast-check": "^4.10.2"
176
+ },
177
+ "peerDependenciesMeta": {
178
+ "fast-check": {
179
+ "optional": true
180
+ }
181
+ },
171
182
  "devDependencies": {
172
183
  "@arethetypeswrong/cli": "^0.18.2",
173
- "@biomejs/biome": "^2.4.12",
184
+ "@biomejs/biome": "^2.5.15",
174
185
  "@classytic/dev-tools": "^0.2.0",
175
186
  "@types/node": "^22.0.0",
176
187
  "@vitest/coverage-v8": "^4.1.4",
177
- "fast-check": "^4.7.0",
188
+ "fast-check": "^4.10.2",
178
189
  "knip": "^6.3.0",
179
190
  "publint": "^0.3.18",
180
191
  "tsdown": "^0.22.14",
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: repo-core
3
+ description: Driver-agnostic repository contracts shared by mongokit, sqlitekit and arc - list envelopes, ErrorContract and error codes, Filter IR, URL query grammar, keyset cursors, idempotency claims, hooks, cache adapter. Use when typing a list response, mapping errors, parsing query strings, or writing a kit or plugin. Triggers - repo-core, ErrorContract, toCanonicalList, parseUrl, Filter IR.
4
+ metadata:
5
+ minVersion: "0.31.1"
6
+ package: "@classytic/repo-core"
7
+ note: "No root entry. Import from the exact subpath."
8
+ ---
9
+
10
+ # @classytic/repo-core
11
+
12
+ The contract layer under every kit. Zero runtime dependencies, tree-shakeable, subpath imports only (`@classytic/repo-core/<subpath>`; there is no root import). App code uses it for envelopes, errors, filters and query parsing; kit authors also use `repository`, `hooks`, `testing`.
13
+
14
+ ## Do not hand-roll
15
+
16
+ | Need | Use |
17
+ |---|---|
18
+ | Wire list shape (`{ method, data, ... }` or `{ data }`) | `toCanonicalList`, `isPaginatedResult` (`/pagination`) |
19
+ | Page result types | `OffsetPaginationResult`, `KeysetPaginationResult`, `PaginatedResult` |
20
+ | Error JSON body | `toErrorContract` -> `ErrorContract` (`/errors`) |
21
+ | Throwable HTTP error | `createError`, `isHttpError` |
22
+ | Status to code | `statusToErrorCode`, `ERROR_CODES` |
23
+ | Duplicate key to 409 | `toDuplicateKeyHttpError`, `DUPLICATE_KEY_DETAIL_CODE` |
24
+ | Version conflict | `VersionConflictError`, `isVersionConflictError` |
25
+ | Query string to filter, sort, page, select | `parseUrl` (`/query-parser`) |
26
+ | Filters and sort to URL params (inverse of `parseUrl`) | `formatQuery`, `stringifyQuery` (`/query-parser`) |
27
+ | Escape or vet user regex | `escapeRegex`, `assessRegex` |
28
+ | Portable filters | `and`, `or`, `eq`, `in_`, `between`, `contains` (`/filter`) |
29
+ | Evaluate a filter in memory | `matchFilter`, `asPredicate` |
30
+ | Tenant filter merge | `buildTenantScope`, `mergeScope` |
31
+ | Portable updates | `update`, `incFields`, `setFields` (`/update`) |
32
+ | Cursor encode, decode, sort checks | `encodeCursor`, `decodeCursor`, `validateKeysetSort` |
33
+ | One keyset page over several sources | `mergeKeysetPages` |
34
+ | Run once per idempotency key | `runIdempotent`, `fingerprintRequest` (`/idempotency`) |
35
+ | Retry transient failures | `withRetry`, `retryingTransaction` (`/repository`) |
36
+ | Batch N+1 lookups | `createBatchLoader` (`/loader`) |
37
+ | Seal a repo you do not own the writes of | `asReadOnlyRepo` |
38
+ | Cache adapter and test double | `CacheAdapter`, `createMemoryCacheAdapter` (`/cache`) |
39
+ | Hook order and event names | `HOOK_PRIORITY`, `HOOK_EVENTS` (`/hooks`) |
40
+ | Publish repo domain events | `registerRepositoryEvents` (`/events`) |
41
+ | Tenant purge as a cleanup step | `definePurgeStep` (`/cleanup`) |
42
+ | Stable content hash | `contentHash`, `stableStringify` (`/hash`) |
43
+ | Tenant config | `resolveTenantConfig` (`/tenant`) |
44
+ | Standard Schema (zod, valibot) check | `validateStandardSchema` (`/schema`) |
45
+ | Lock or usage test doubles | `createMemoryLockAdapter`, `createMemoryUsageStore` |
46
+ | Prove a kit conforms | `runStandardRepoConformance` (`/testing`) |
47
+
48
+ ## Golden path
49
+
50
+ List route (kit result to wire):
51
+
52
+ ```ts
53
+ import { toCanonicalList } from '@classytic/repo-core/pagination';
54
+ import { parseUrl } from '@classytic/repo-core/query-parser';
55
+
56
+ const q = parseUrl(req.query, { allowedFilterFields: ['status', 'createdAt'], fieldTypes: { createdAt: 'date' }, maxLimit: 200 });
57
+ const { filter, ...rest } = q;
58
+ reply.send(toCanonicalList(await repo.getAll({ ...rest, filters: filter }))); // paged results keep `method`; a bare array becomes { data }
59
+ ```
60
+
61
+ Errors (throw `HttpError`, serialize once at the edge):
62
+
63
+ ```ts
64
+ import { createError, toErrorContract } from '@classytic/repo-core/errors';
65
+ const err = createError(409, 'Already paid');
66
+ err.code = 'order.already_paid';
67
+ reply.code(409).send(toErrorContract(err)); // { code, message, status, details?, meta? }
68
+ ```
69
+
70
+ Filter IR (same on every kit; native filters also work):
71
+
72
+ ```ts
73
+ import { and, eq, in_, between } from '@classytic/repo-core/filter';
74
+ await repo.findAll!(and(eq('status', 'open'), in_('kind', ['a', 'b']), between('total', 10, 100)));
75
+ ```
76
+
77
+ Idempotent side effect:
78
+
79
+ ```ts
80
+ import { fingerprintRequest, runIdempotent } from '@classytic/repo-core/idempotency';
81
+ const run = await runIdempotent({ store, identity: { operation: 'charge', key, scope: { orgId } },
82
+ requestFingerprint: fingerprintRequest(body), execute: async () => charge(body) });
83
+ // run.outcome: 'executed' | 'replayed' | 'in_flight' (retryAfterMs) | 'exhausted'
84
+ ```
85
+
86
+ ## Gotchas
87
+
88
+ - Envelope rows are in `data` (never `docs`). Narrow with `result.method`.
89
+ - `parseUrl` returns `filter` (Filter IR) but `getAll` reads `filters`: rename the key. mongokit's `QueryParser` already returns `filters`.
90
+ - `ErrorContract.code` has three families: `ERROR_CODES` (`conflict`), `ARC_ERROR_CODES` (`arc.conflict`), domain strings (`order.already_paid`). Switch on code, not message.
91
+ - `createError(status, message)` takes two arguments; set `code`, `meta`, `validationErrors` on the result.
92
+ - `CURSOR_ERROR_CODES`: `repo.cursor.invalid` and `repo.cursor.scope_mismatch` (400); restart paging on both.
93
+ - `before` and `mode` are reserved query params (not filters).
94
+ - `update({})` with no mutation throws; `undefined` values in `set` are dropped (use `unset` to clear).
95
+ - Duplicate-key values are PII: `exposeValues` is opt-in.
96
+ - A kit declares what it supports in `capabilities`; feature-detect instead of try and catch.
97
+
98
+ ## Never
99
+
100
+ - Re-declare `HttpError`, `ErrorContract` or the page types in an app or kit.
101
+ - Import a root `@classytic/repo-core` (does not exist) or `dist/` paths.
102
+ - Parse `req.query` by hand for list routes.
103
+
104
+ ## References
105
+
106
+ - `references/contracts.md`: read for envelope shapes, error codes, query grammar operators, cursor behavior.
107
+ - `references/kit-authoring.md`: read when writing a kit, plugin, conformance harness, hooks, cache or purge step.
108
+ - Kit behavior: skill `mongokit`. HTTP wiring: skill `arc`.
@@ -0,0 +1,69 @@
1
+ # Contracts: envelopes, errors, query grammar
2
+
3
+ ## Page envelopes (`/pagination`)
4
+
5
+ ```
6
+ offset: { method: 'offset', data, page, limit, total, pages, hasNext, hasPrev }
7
+ keyset: { method: 'keyset', data, limit, hasMore, next, prev?, hasPrev?, end? }
8
+ aggregate: { method: 'aggregate', data, ... } // page or keyset shaped, from aggregatePaginate
9
+ bare: { data } // BareListResult, from a non-paged list
10
+ ```
11
+
12
+ `OffsetPaginationResult<T, TExtra>` and `KeysetPaginationResult<T, TExtra>` take a typed extras bag (a kit may add `warning`). `PaginatedResult<T>` is the
13
+ wire union; discriminate with `'method' in res`. `prev`, `hasPrev`, `end` are optional: absent means the kit does not answer, not "none".
14
+ `end` is the cursor at the page's last row even when `next` is null (resume a change feed from it).
15
+
16
+ `toCanonicalList(x)` copies a paged result or wraps a bare array. Call it once at the HTTP edge; clients type responses as `PaginatedResult<T>`.
17
+
18
+ Cursors: `encodeCursor`, `decodeCursor`, `validateCursorSort`, `validateCursorVersion` (kit authors). Offset math: `calculateSkip`, `calculateTotalPages`,
19
+ `validateLimit`, `validatePage`. `mergeKeysetPages(sources, sort, { limit, cursor, secret })` returns one page with ONE composite cursor bound to the
20
+ source names, scopes and sort (HMAC-signed when `secret` is set); it returns `{ docs, next, hasNext }`.
21
+
22
+ ## Errors (`/errors`)
23
+
24
+ Throwable `HttpError`: `status`, optional `code`, `meta`, `validationErrors[]` (`validator`, `error`, `path`), `duplicate` (`fields`, `values?`).
25
+ Wire `ErrorContract`: `{ code, message, status?, details?: ErrorDetail[], correlationId?, meta? }`, flat, no `{ error: {} }` wrapper.
26
+ `toErrorContract(err)` maps `validationErrors` and `duplicate.fields` to `details` (`path`, `code`, `message`); `status` without `code` derives the code
27
+ through `statusToErrorCode`.
28
+
29
+ | Family | Examples | Source |
30
+ |---|---|---|
31
+ | cross-cutting | `validation_error`, `not_found`, `conflict`, `unauthorized`, `forbidden`, `rate_limited`, `idempotency_conflict`, `precondition_failed`, `internal_error`, `service_unavailable`, `timeout` | `ERROR_CODES` |
32
+ | arc | `arc.conflict`, `arc.validation_error`, `arc.idempotency_key_reused`, `arc.org.access_denied`, `arc.pagination.invalid_mode` | `ARC_ERROR_CODES` |
33
+ | reasons | `ORG_CONTEXT_REQUIRED`, `MIXED_UPDATE_SHAPE`, `ALL_FIELDS_STRIPPED` | `ARC_REASON_CODES` |
34
+ | cursors | `repo.cursor.invalid`, `repo.cursor.scope_mismatch` | `CURSOR_ERROR_CODES` |
35
+ | domain | `order.already_paid` | your package, dotted lowercase |
36
+
37
+ A duplicate key can surface as `conflict` (thrown by a kit) or `arc.conflict` (raised by the HTTP layer); clients treat both as a 409. Validate a
38
+ contract body in tests with `errorContractSchema` and `errorDetailSchema`. Hosts that accept unmapped library errors (auth providers, framework 404s) normalize
39
+ them into the contract at the edge.
40
+
41
+ `conservativeMongoIsDuplicateKey` and `conservativeMongoIsTransientConflict` classify raw driver errors when you have no kit repository to ask.
42
+
43
+ ## Query grammar (`/query-parser`)
44
+
45
+ `parseUrl(input, options)` accepts `URLSearchParams`, an iterable of pairs, or a record. Result: `{ filter, limit, sort?, select?, populate?, page?, after?, search? }`.
46
+
47
+ | URL | Meaning |
48
+ |---|---|
49
+ | `status=active` | equals; repeated keys mean in |
50
+ | `age[gte]=18&age[lt]=65` | range |
51
+ | `role[in]=a,b`, `role[nin]=a` | membership |
52
+ | `name[contains]=jo`, `[startsWith]`, `[endsWith]`, `[ieq]` | literal, case-insensitive text |
53
+ | `price[between]=10,100` | inclusive range |
54
+ | `deletedAt[exists]=false` | null or missing |
55
+ | `title[regex]=^a&title[options]=i` | regex, checked for ReDoS |
56
+ | `sort=-createdAt,name`, `select=a,b,-c` | sort and projection |
57
+ | `page=2&limit=50`, `after=<cursor>`, `before=<cursor>`, `mode=keyset` | paging (reserved) |
58
+
59
+ Client side, `formatQuery({ filters: [{ field, op?, value }], sort, page, limit, search })` emits exactly these params (`stringifyQuery` gives the string). `between: [from, to]` is sent as `gte` + `lte`; `in`/`nin` take arrays; empty values are omitted; unrepresentable values throw `QueryGrammarError`.
60
+
61
+ Options: `allowedFilterFields`, `allowedOperators`, `allowedSortFields`, `fieldTypes` (`boolean`, `date`, `number`...), `defaultLimit` (20), `maxLimit` (1000),
62
+ `maxSearchLength` (200), `maxRegexLength`. Every allow-list refuses with a `QueryGrammarError` (400, `INVALID_QUERY_INPUT`); nothing is silently dropped, nothing truncated.
63
+ Repeated control params are refused. Lower-level readers: `readFilterClauses`, `readPageRequest`, `readSort`, `readSelect`, `clausesToFilter`.
64
+
65
+ ## Filter IR (`/filter`)
66
+
67
+ Builders: `eq ne gt gte lt lte in_ nin exists contains startsWith endsWith iEq like regex between isNull isNotNull and or not anyOf noneOf raw TRUE FALSE`.
68
+ `and()` with no children is `TRUE`. `recordToFilter(record)` converts a flat record. `walkFilter`, `mapFilter`, `collectFields` traverse; `coerceFilterDates` turns ISO strings
69
+ into dates. `matchFilter(filter, row)` evaluates in memory (policy checks, tests). `raw(sql, params)` is SQL-kit only.
@@ -0,0 +1,75 @@
1
+ # Writing a kit, plugin or conformance harness
2
+
3
+ ## Repository base (`/repository`)
4
+
5
+ A kit extends `RepositoryBase` and implements `MinimalRepo` (`getAll`, `getById`, `create`, `update`, `delete`) plus as much of `StandardRepo` as the backend
6
+ supports. Every operation goes: `_buildContext` (runs `before:*` hooks and policy plugins) -> native call -> `_emitAfter`. Options on `RepositoryBaseOptions`:
7
+ `name`, `plugins`, `hooks` (`sync` or `async`), `pluginOrderChecks` (`warn`, `throw`, `off`), `schema`, `updateSchema` (any Standard Schema validator), `events`.
8
+ Validation runs at `HOOK_PRIORITY.VALIDATION`; event publishing never fails the operation (failures go to `error:events`).
9
+
10
+ Do not narrow an existing contract type: additive optional fields are safe, renames and new required fields are breaking. Method signatures use `unknown` for
11
+ sessions and `readonly string[]` for arrays so kits stay assignable.
12
+
13
+ `StandardRepo` extras (optional unless noted): `claim`, `claimVersion` (required CAS), `applyTransition`, `findOneAndUpdate`, `getOne`, `getByIds`, `findAll`, `count`, `exists`,
14
+ `distinct`, `getOrCreate`, `createMany`, `updateMany`, `deleteMany`, `bulkUpsert`, `purgeByField`, `purgeByFilter`, `archiveByFilter`, `aggregate`, `lookupPopulate`,
15
+ `isDuplicateKeyError`, `isTransientConflictError`. Declare what works in `capabilities` (`transactions`, `upsert`, `aggregate`, `aggregateOps`, `readOnly`, `arrayOperators`...).
16
+
17
+ ## Plugins and hooks (`/hooks`)
18
+
19
+ ```ts
20
+ import { HOOK_EVENTS, HOOK_PRIORITY } from '@classytic/repo-core/hooks';
21
+ import type { Plugin } from '@classytic/repo-core/repository';
22
+
23
+ export const stampOrg = (orgId: string): Plugin => ({
24
+ name: 'stamp-org',
25
+ apply(repo) {
26
+ repo.on(HOOK_EVENTS.BEFORE_CREATE, (ctx) => { ctx.data = { ...ctx.data, organizationId: orgId }; }, { priority: HOOK_PRIORITY.POLICY });
27
+ },
28
+ });
29
+ ```
30
+
31
+ Priorities: `POLICY` 100, `VALIDATION` 150, `CACHE` 200, `OBSERVABILITY` 300, `DEFAULT` 500 (lower runs first). Events per op: `before:`, `after:`, `error:` for create, update, delete,
32
+ getById, getAll, createMany, updateMany, deleteMany, findOneAndUpdate, restore. Listening to an event a kit never emits is a silent no-op.
33
+ `PLUGIN_ORDER_CONSTRAINTS`: soft-delete before batch-operations; multi-tenant before cache and before soft-delete. `validatePluginOrder` enforces them.
34
+
35
+ Domain events: pass `events: { transport, source }` and every mutating op publishes `<resource>.<verb>`. Do not also enable arc's auto events for the same resource (double publish).
36
+
37
+ ## Cache (`/cache`)
38
+
39
+ `CacheAdapter` is `{ get, set(key, value, ttlSeconds?), delete, clear?, addToSet?, increment? }`. `cachePlugin` options: `adapter`, `enabled`, `invalidating`, `defaults`
40
+ (`staleTime`, `gcTime`, `swr`, `tags`, `bypass`), `perOpDefaults`, `prefix`, `autoTagsFromScope`, `jitter`. `withTimeout(adapter, ...)` bounds a slow cache so it degrades to a miss.
41
+ `createMemoryCacheAdapter` and `createTtlMemo` are in-process helpers.
42
+
43
+ ## Idempotency (`/idempotency`)
44
+
45
+ `IdempotencyClaimStore` is the persistence contract (kits ship one: mongokit `createIdempotencyStore`, `createMemoryIdempotencyStore` in repo-core). `runIdempotent` drives
46
+ claim, execute, complete with leases (`leaseMs`, default 30 s), checkpoints (`attempt.checkpoint(progress)`) and replay. A reused key with a different body fails
47
+ `FINGERPRINT_MISMATCH`. `classifyFailure` returns `terminal` (store the failure), `transient` (release for retry) or `ambiguous` (keep the lease). `IdempotencyError.code` is one of
48
+ `FINGERPRINT_MISMATCH`, `MISSING_REPLAY`, `LEASE_LOST`, `ALREADY_TERMINAL`, `INVALID_CLAIM`, `UNFINGERPRINTABLE`.
49
+
50
+ ## Other subpaths
51
+
52
+ | Subpath | Content |
53
+ |---|---|
54
+ | `/lock` | `LockAdapter`, `createMemoryLockAdapter` |
55
+ | `/usage` | `UsageStore`, `usagePeriod`, `createMemoryUsageStore` |
56
+ | `/sync` | change-log and command-stream store contracts |
57
+ | `/loader` | `createBatchLoader({ batch, maxBatchSize, requireAllResolved })` |
58
+ | `/cleanup` | `definePurgeStep` (fail-closed scope, preview, chunked purge, proof of absence) |
59
+ | `/adapter` | `DataAdapter` contract, `asRepositoryLike`, `isRepository` for arc |
60
+ | `/schema` | `SchemaGenerator`, `validateStandardSchema`, field rule helpers |
61
+ | `/operations` | `CORE_OP_REGISTRY`, `describe`, `listOperations` |
62
+ | `/aggregate` | aggregation cursor and normalization helpers |
63
+ | `/lookup` | `LookupSpec`, `LookupPopulateResult` types |
64
+ | `/context` | `RepositoryContext` type |
65
+
66
+ ## Conformance (`/testing`, needs the optional peer `fast-check` for property suites)
67
+
68
+ ```ts
69
+ import { runStandardRepoConformance } from '@classytic/repo-core/testing';
70
+ runStandardRepoConformance({ name: 'my-kit', idField: 'id', features, setup, makeDoc }); // setup() returns { repo, cleanup }
71
+ ```
72
+
73
+ Gated groups (aggregate ops, bulkUpsert, readModel, mergeKeyset) run only when the kit declares the capability. Other suites: `runLockAdapterConformance`, `runUsageStoreContract`,
74
+ `runPurgeConformance`, `runChangeLogStoreConformance`, `runQueryGrammarConformance`. Use `harness.makeDoc` for fixtures and `harness.idField` for ids; never inline a kit shape.
75
+ An unsupported op must throw a clear error naming the kit and the alternative, never approximate silently.