@classytic/repo-core 0.31.1 → 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.
- package/CHANGELOG.md +14 -0
- package/dist/_virtual/_rolldown/runtime.mjs +1 -2
- package/dist/adapter/index.mjs +1 -2
- package/dist/adapter/types.d.mts +0 -2
- package/dist/adapter/widen.d.mts +0 -2
- package/dist/adapter/widen.mjs +1 -2
- package/dist/aggregate/keyset.d.mts +0 -2
- package/dist/aggregate/keyset.mjs +1 -2
- package/dist/aggregate/normalize.d.mts +0 -2
- package/dist/aggregate/normalize.mjs +1 -2
- package/dist/better-auth/index.d.mts +0 -2
- package/dist/better-auth/index.mjs +1 -2
- package/dist/cache/engine.d.mts +0 -2
- package/dist/cache/engine.mjs +1 -2
- package/dist/cache/envelope.mjs +1 -2
- package/dist/cache/keys.mjs +1 -2
- package/dist/cache/memo.d.mts +0 -2
- package/dist/cache/memo.mjs +1 -2
- package/dist/cache/memory-adapter.d.mts +0 -2
- package/dist/cache/memory-adapter.mjs +1 -2
- package/dist/cache/options.d.mts +0 -2
- package/dist/cache/options.mjs +1 -2
- package/dist/cache/plugin/context.d.mts +0 -2
- package/dist/cache/plugin/context.mjs +1 -2
- package/dist/cache/plugin/index.d.mts +0 -2
- package/dist/cache/plugin/index.mjs +1 -2
- package/dist/cache/plugin/invalidation-hooks.mjs +1 -2
- package/dist/cache/plugin/read-hooks.mjs +1 -2
- package/dist/cache/plugin/swr.mjs +1 -2
- package/dist/cache/runtime.d.mts +0 -2
- package/dist/cache/runtime.mjs +0 -1
- package/dist/cache/stable-stringify.d.mts +0 -2
- package/dist/cache/stable-stringify.mjs +1 -2
- package/dist/cache/tag-index.mjs +1 -2
- package/dist/cache/timeout-adapter.d.mts +0 -2
- package/dist/cache/timeout-adapter.mjs +1 -2
- package/dist/cache/types.d.mts +0 -2
- package/dist/cache/version-store.mjs +1 -2
- package/dist/cleanup/define-purge-step.d.mts +0 -2
- package/dist/cleanup/define-purge-step.mjs +1 -2
- package/dist/cleanup/index.mjs +1 -2
- package/dist/cleanup/types.d.mts +0 -2
- package/dist/context/types.d.mts +0 -2
- package/dist/errors/codes.d.mts +0 -2
- package/dist/errors/codes.mjs +1 -2
- package/dist/errors/conflict.d.mts +0 -2
- package/dist/errors/conflict.mjs +1 -2
- package/dist/errors/contract.d.mts +0 -2
- package/dist/errors/contract.mjs +1 -2
- package/dist/errors/create-error.d.mts +0 -2
- package/dist/errors/create-error.mjs +1 -2
- package/dist/errors/duplicate-key.d.mts +0 -2
- package/dist/errors/duplicate-key.mjs +1 -2
- package/dist/errors/schema.d.mts +0 -2
- package/dist/errors/schema.mjs +1 -2
- package/dist/errors/types.d.mts +0 -2
- package/dist/events/emit.d.mts +0 -2
- package/dist/events/emit.mjs +1 -2
- package/dist/events/index.mjs +1 -2
- package/dist/events/types.d.mts +0 -2
- package/dist/filter/builders.d.mts +0 -2
- package/dist/filter/builders.mjs +1 -2
- package/dist/filter/coerce-dates.d.mts +0 -2
- package/dist/filter/coerce-dates.mjs +1 -2
- package/dist/filter/from-record.d.mts +0 -2
- package/dist/filter/from-record.mjs +1 -2
- package/dist/filter/guard.d.mts +0 -2
- package/dist/filter/guard.mjs +1 -2
- package/dist/filter/match-record.d.mts +0 -2
- package/dist/filter/match-record.mjs +1 -2
- package/dist/filter/match.d.mts +0 -2
- package/dist/filter/match.mjs +1 -2
- package/dist/filter/scope.d.mts +0 -2
- package/dist/filter/scope.mjs +1 -2
- package/dist/filter/types.d.mts +0 -2
- package/dist/filter/walk.d.mts +0 -2
- package/dist/filter/walk.mjs +1 -2
- package/dist/hash/index.d.mts +0 -2
- package/dist/hash/index.mjs +1 -2
- package/dist/hooks/engine.d.mts +0 -2
- package/dist/hooks/engine.mjs +1 -2
- package/dist/hooks/events.d.mts +0 -2
- package/dist/hooks/events.mjs +1 -2
- package/dist/hooks/priority.d.mts +0 -2
- package/dist/hooks/priority.mjs +1 -2
- package/dist/hooks/types.d.mts +0 -2
- package/dist/idempotency/index.d.mts +0 -2
- package/dist/idempotency/index.mjs +1 -2
- package/dist/loader/batch-loader.d.mts +0 -2
- package/dist/loader/batch-loader.mjs +1 -2
- package/dist/loader/index.mjs +1 -2
- package/dist/lock/index.d.mts +0 -2
- package/dist/lock/index.mjs +1 -2
- package/dist/lookup/types.d.mts +0 -2
- package/dist/operations/index.mjs +1 -2
- package/dist/operations/registry.d.mts +0 -2
- package/dist/operations/registry.mjs +1 -2
- package/dist/operations/types.d.mts +0 -2
- package/dist/pagination/canonical.d.mts +0 -2
- package/dist/pagination/canonical.mjs +1 -2
- package/dist/pagination/cursor.d.mts +0 -2
- package/dist/pagination/cursor.mjs +1 -2
- package/dist/pagination/keyset.d.mts +0 -2
- package/dist/pagination/keyset.mjs +1 -2
- package/dist/pagination/merge-keyset.d.mts +0 -2
- package/dist/pagination/merge-keyset.mjs +1 -2
- package/dist/pagination/offset.d.mts +0 -2
- package/dist/pagination/offset.mjs +1 -2
- package/dist/pagination/types.d.mts +0 -2
- package/dist/plugins/index.mjs +1 -2
- package/dist/plugins/tenant-helpers.d.mts +0 -2
- package/dist/plugins/tenant-helpers.mjs +1 -2
- package/dist/query-parser/coerce.d.mts +0 -2
- package/dist/query-parser/coerce.mjs +1 -2
- package/dist/query-parser/errors.d.mts +0 -2
- package/dist/query-parser/errors.mjs +1 -2
- package/dist/query-parser/format-query.d.mts +30 -0
- package/dist/query-parser/format-query.mjs +90 -0
- package/dist/query-parser/grammar.d.mts +0 -2
- package/dist/query-parser/grammar.mjs +1 -2
- package/dist/query-parser/index.d.mts +2 -1
- package/dist/query-parser/index.mjs +2 -1
- package/dist/query-parser/parse-url.d.mts +0 -2
- package/dist/query-parser/parse-url.mjs +1 -2
- package/dist/query-parser/regex-safety.d.mts +0 -2
- package/dist/query-parser/regex-safety.mjs +1 -2
- package/dist/query-parser/reserved.d.mts +0 -2
- package/dist/query-parser/reserved.mjs +1 -2
- package/dist/query-parser/types.d.mts +0 -2
- package/dist/repository/agg-output.d.mts +0 -2
- package/dist/repository/agg-output.mjs +1 -2
- package/dist/repository/archive.d.mts +0 -2
- package/dist/repository/archive.mjs +1 -2
- package/dist/repository/base.d.mts +0 -2
- package/dist/repository/base.mjs +1 -2
- package/dist/repository/bulk-upsert.d.mts +0 -2
- package/dist/repository/bulk-upsert.mjs +1 -2
- package/dist/repository/capabilities.d.mts +0 -2
- package/dist/repository/distribution.d.mts +0 -2
- package/dist/repository/distribution.mjs +1 -2
- package/dist/repository/options.d.mts +0 -2
- package/dist/repository/options.mjs +1 -2
- package/dist/repository/plugin-types.d.mts +0 -2
- package/dist/repository/plugin-types.mjs +1 -2
- package/dist/repository/purge.d.mts +0 -2
- package/dist/repository/purge.mjs +1 -2
- package/dist/repository/read-only.d.mts +0 -2
- package/dist/repository/read-only.mjs +1 -2
- package/dist/repository/resilience.d.mts +0 -2
- package/dist/repository/resilience.mjs +1 -2
- package/dist/repository/retrying-transaction.d.mts +0 -2
- package/dist/repository/retrying-transaction.mjs +1 -2
- package/dist/repository/types.d.mts +0 -2
- package/dist/schema/field-rules.d.mts +0 -2
- package/dist/schema/field-rules.mjs +1 -2
- package/dist/schema/generator.d.mts +0 -2
- package/dist/schema/generator.mjs +1 -2
- package/dist/schema/standard-schema.d.mts +0 -2
- package/dist/schema/standard-schema.mjs +1 -2
- package/dist/schema/types.d.mts +0 -2
- package/dist/sync/index.d.mts +0 -2
- package/dist/sync/index.mjs +0 -0
- package/dist/tenant/index.mjs +1 -2
- package/dist/tenant/resolve.d.mts +0 -2
- package/dist/tenant/resolve.mjs +1 -2
- package/dist/tenant/types.d.mts +0 -2
- package/dist/testing/change-log-conformance.d.mts +0 -2
- package/dist/testing/change-log-conformance.mjs +1 -2
- package/dist/testing/command-stream-conformance.d.mts +0 -2
- package/dist/testing/command-stream-conformance.mjs +1 -2
- package/dist/testing/conformance.d.mts +0 -2
- package/dist/testing/conformance.mjs +1 -2
- package/dist/testing/data-access-conformance.d.mts +0 -2
- package/dist/testing/data-access-conformance.mjs +1 -2
- package/dist/testing/idempotency-conformance.d.mts +0 -2
- package/dist/testing/idempotency-conformance.mjs +1 -2
- package/dist/testing/lock-conformance.d.mts +0 -2
- package/dist/testing/lock-conformance.mjs +1 -2
- package/dist/testing/merge-keyset-conformance.d.mts +0 -2
- package/dist/testing/merge-keyset-conformance.mjs +1 -2
- package/dist/testing/pagination-property-conformance.d.mts +0 -2
- package/dist/testing/pagination-property-conformance.mjs +1 -2
- package/dist/testing/purge-conformance.d.mts +0 -2
- package/dist/testing/purge-conformance.mjs +1 -2
- package/dist/testing/query-grammar-conformance.d.mts +0 -2
- package/dist/testing/query-grammar-conformance.mjs +1 -2
- package/dist/testing/read-model-property-conformance.d.mts +0 -2
- package/dist/testing/read-model-property-conformance.mjs +1 -2
- package/dist/testing/types.d.mts +0 -2
- package/dist/testing/usage-conformance.d.mts +0 -2
- package/dist/testing/usage-conformance.mjs +1 -2
- package/dist/update/builders.d.mts +0 -2
- package/dist/update/builders.mjs +1 -2
- package/dist/update/compile.d.mts +0 -2
- package/dist/update/compile.mjs +1 -2
- package/dist/update/guard.d.mts +0 -2
- package/dist/update/guard.mjs +1 -2
- package/dist/update/types.d.mts +0 -2
- package/dist/usage/index.d.mts +0 -2
- package/dist/usage/index.mjs +1 -2
- package/llms.txt +29 -0
- package/package.json +7 -4
- package/skills/repo-core/SKILL.md +108 -0
- package/skills/repo-core/references/contracts.md +69 -0
- package/skills/repo-core/references/kit-authoring.md +75 -0
package/dist/update/compile.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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 };
|
package/dist/update/guard.d.mts
CHANGED
|
@@ -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 };
|
package/dist/update/guard.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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 };
|
package/dist/update/types.d.mts
CHANGED
|
@@ -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 };
|
package/dist/usage/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/usage/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
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.
|
|
3
|
+
"version": "0.32.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,
|
|
7
7
|
"files": [
|
|
8
8
|
"dist",
|
|
9
|
+
"skills",
|
|
10
|
+
"llms.txt",
|
|
9
11
|
"README.md",
|
|
10
12
|
"LICENSE",
|
|
11
13
|
"CHANGELOG.md"
|
|
@@ -161,15 +163,16 @@
|
|
|
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
|
},
|
|
171
174
|
"peerDependencies": {
|
|
172
|
-
"fast-check": "^4.
|
|
175
|
+
"fast-check": "^4.10.2"
|
|
173
176
|
},
|
|
174
177
|
"peerDependenciesMeta": {
|
|
175
178
|
"fast-check": {
|
|
@@ -182,7 +185,7 @@
|
|
|
182
185
|
"@classytic/dev-tools": "^0.2.0",
|
|
183
186
|
"@types/node": "^22.0.0",
|
|
184
187
|
"@vitest/coverage-v8": "^4.1.4",
|
|
185
|
-
"fast-check": "^4.
|
|
188
|
+
"fast-check": "^4.10.2",
|
|
186
189
|
"knip": "^6.3.0",
|
|
187
190
|
"publint": "^0.3.18",
|
|
188
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.
|