@classytic/repo-core 0.2.0 → 0.4.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 (87) hide show
  1. package/CHANGELOG.md +363 -0
  2. package/README.md +28 -7
  3. package/dist/adapter/index.d.mts +3 -0
  4. package/dist/adapter/index.mjs +2 -0
  5. package/dist/adapter/types.d.mts +222 -0
  6. package/dist/adapter/widen.d.mts +22 -0
  7. package/dist/adapter/widen.mjs +26 -0
  8. package/dist/aggregate/index.d.mts +3 -0
  9. package/dist/aggregate/index.mjs +3 -0
  10. package/dist/aggregate/keyset.d.mts +57 -0
  11. package/dist/aggregate/keyset.mjs +45 -0
  12. package/dist/aggregate/normalize.d.mts +24 -0
  13. package/dist/aggregate/normalize.mjs +28 -0
  14. package/dist/better-auth/index.d.mts +110 -0
  15. package/dist/better-auth/index.mjs +71 -0
  16. package/dist/cache/engine.d.mts +127 -0
  17. package/dist/cache/engine.mjs +235 -0
  18. package/dist/cache/envelope.mjs +32 -0
  19. package/dist/cache/index.d.mts +7 -2
  20. package/dist/cache/index.mjs +6 -2
  21. package/dist/cache/keys.mjs +131 -0
  22. package/dist/cache/memory-adapter.mjs +41 -7
  23. package/dist/cache/options.d.mts +112 -0
  24. package/dist/cache/options.mjs +25 -0
  25. package/dist/cache/plugin/context.d.mts +18 -0
  26. package/dist/cache/plugin/context.mjs +121 -0
  27. package/dist/cache/plugin/index.d.mts +86 -0
  28. package/dist/cache/plugin/index.mjs +78 -0
  29. package/dist/cache/plugin/invalidation-hooks.mjs +35 -0
  30. package/dist/cache/plugin/read-hooks.mjs +96 -0
  31. package/dist/cache/plugin/swr.mjs +20 -0
  32. package/dist/cache/runtime.d.mts +43 -0
  33. package/dist/cache/runtime.mjs +14 -0
  34. package/dist/cache/tag-index.mjs +84 -0
  35. package/dist/cache/timeout-adapter.d.mts +30 -0
  36. package/dist/cache/timeout-adapter.mjs +58 -0
  37. package/dist/cache/types.d.mts +45 -0
  38. package/dist/cache/version-store.mjs +57 -0
  39. package/dist/errors/contract.d.mts +37 -0
  40. package/dist/errors/contract.mjs +75 -0
  41. package/dist/errors/index.d.mts +4 -2
  42. package/dist/errors/index.mjs +4 -1
  43. package/dist/errors/schema.d.mts +101 -0
  44. package/dist/errors/schema.mjs +78 -0
  45. package/dist/errors/types.d.mts +113 -8
  46. package/dist/errors/types.mjs +29 -0
  47. package/dist/filter/match.mjs +38 -2
  48. package/dist/pagination/canonical.d.mts +35 -0
  49. package/dist/pagination/canonical.mjs +26 -0
  50. package/dist/pagination/cursor.mjs +4 -1
  51. package/dist/pagination/index.d.mts +3 -2
  52. package/dist/pagination/index.mjs +2 -1
  53. package/dist/pagination/types.d.mts +57 -3
  54. package/dist/plugins/index.d.mts +2 -0
  55. package/dist/plugins/index.mjs +2 -0
  56. package/dist/plugins/tenant-helpers.d.mts +63 -0
  57. package/dist/plugins/tenant-helpers.mjs +84 -0
  58. package/dist/query-parser/index.d.mts +2 -1
  59. package/dist/query-parser/index.mjs +2 -1
  60. package/dist/query-parser/parse-url.mjs +13 -11
  61. package/dist/query-parser/reserved.d.mts +43 -0
  62. package/dist/query-parser/reserved.mjs +56 -0
  63. package/dist/repository/agg-output.d.mts +63 -0
  64. package/dist/repository/agg-output.mjs +89 -0
  65. package/dist/repository/base.mjs +21 -0
  66. package/dist/repository/index.d.mts +4 -2
  67. package/dist/repository/index.mjs +3 -1
  68. package/dist/repository/options.d.mts +62 -0
  69. package/dist/repository/options.mjs +57 -0
  70. package/dist/repository/types.d.mts +935 -48
  71. package/dist/schema/field-rules.d.mts +60 -9
  72. package/dist/schema/field-rules.mjs +121 -10
  73. package/dist/schema/generator.d.mts +72 -0
  74. package/dist/schema/generator.mjs +16 -0
  75. package/dist/schema/index.d.mts +3 -2
  76. package/dist/schema/index.mjs +3 -2
  77. package/dist/schema/types.d.mts +77 -3
  78. package/dist/tenant/index.d.mts +3 -0
  79. package/dist/tenant/index.mjs +2 -0
  80. package/dist/tenant/resolve.d.mts +27 -0
  81. package/dist/tenant/resolve.mjs +69 -0
  82. package/dist/tenant/types.d.mts +142 -0
  83. package/dist/testing/conformance.mjs +666 -17
  84. package/dist/testing/index.d.mts +2 -2
  85. package/dist/testing/types.d.mts +99 -2
  86. package/package.json +27 -1
  87. package/dist/cache/stable-stringify.d.mts +0 -15
@@ -0,0 +1,43 @@
1
+ //#region src/query-parser/reserved.d.ts
2
+ /**
3
+ * URL parameter keys reserved by the framework — never parsed as
4
+ * filter predicates. Two categories:
5
+ *
6
+ * 1. **Pagination / list-control params** — `page`, `limit`, `after`,
7
+ * `sort`, `select`, `populate`, `search`. Universal across kits;
8
+ * every backend agrees on their meaning.
9
+ *
10
+ * 2. **Resource-dispatch verbs** — `_count`, `_distinct`, `_exists`.
11
+ * Arc-style frameworks pick the repo method from these URL keys
12
+ * (list vs count vs distinct vs exists).
13
+ *
14
+ * **Why an explicit allowlist, not a `_*` namespace.** MongoDB's `_id`
15
+ * (and every kit's analog) is a legitimate filter field; user-defined
16
+ * `_internal`, `_meta`, `_v` fields are common in real schemas. A
17
+ * blanket `key.startsWith('_')` rule silently drops filters on these.
18
+ * Adding a new dispatch verb here is a deliberate ecosystem-wide
19
+ * change — small, audited, and only needed every few major versions.
20
+ */
21
+ /**
22
+ * Reserved top-level URL keys handled outside the filter pipeline.
23
+ * Pagination + sort + select + populate + search + resource-dispatch
24
+ * verbs (`_count`, `_distinct`, `_exists`).
25
+ */
26
+ declare const STANDARD_RESERVED_PARAMS: ReadonlySet<string>;
27
+ /**
28
+ * True when `key` is a framework-reserved URL parameter and should be
29
+ * skipped during filter parsing.
30
+ *
31
+ * Use in any URL-parser implementation that decides "is this a filter
32
+ * predicate or a control flag?":
33
+ *
34
+ * ```ts
35
+ * for (const [key, value] of params.entries()) {
36
+ * if (isControlParam(key)) continue; // skip page, limit, _count, ...
37
+ * // … parse as filter predicate
38
+ * }
39
+ * ```
40
+ */
41
+ declare function isControlParam(key: string): boolean;
42
+ //#endregion
43
+ export { STANDARD_RESERVED_PARAMS, isControlParam };
@@ -0,0 +1,56 @@
1
+ //#region src/query-parser/reserved.ts
2
+ /**
3
+ * URL parameter keys reserved by the framework — never parsed as
4
+ * filter predicates. Two categories:
5
+ *
6
+ * 1. **Pagination / list-control params** — `page`, `limit`, `after`,
7
+ * `sort`, `select`, `populate`, `search`. Universal across kits;
8
+ * every backend agrees on their meaning.
9
+ *
10
+ * 2. **Resource-dispatch verbs** — `_count`, `_distinct`, `_exists`.
11
+ * Arc-style frameworks pick the repo method from these URL keys
12
+ * (list vs count vs distinct vs exists).
13
+ *
14
+ * **Why an explicit allowlist, not a `_*` namespace.** MongoDB's `_id`
15
+ * (and every kit's analog) is a legitimate filter field; user-defined
16
+ * `_internal`, `_meta`, `_v` fields are common in real schemas. A
17
+ * blanket `key.startsWith('_')` rule silently drops filters on these.
18
+ * Adding a new dispatch verb here is a deliberate ecosystem-wide
19
+ * change — small, audited, and only needed every few major versions.
20
+ */
21
+ /**
22
+ * Reserved top-level URL keys handled outside the filter pipeline.
23
+ * Pagination + sort + select + populate + search + resource-dispatch
24
+ * verbs (`_count`, `_distinct`, `_exists`).
25
+ */
26
+ const STANDARD_RESERVED_PARAMS = new Set([
27
+ "page",
28
+ "limit",
29
+ "after",
30
+ "sort",
31
+ "select",
32
+ "populate",
33
+ "search",
34
+ "_count",
35
+ "_distinct",
36
+ "_exists"
37
+ ]);
38
+ /**
39
+ * True when `key` is a framework-reserved URL parameter and should be
40
+ * skipped during filter parsing.
41
+ *
42
+ * Use in any URL-parser implementation that decides "is this a filter
43
+ * predicate or a control flag?":
44
+ *
45
+ * ```ts
46
+ * for (const [key, value] of params.entries()) {
47
+ * if (isControlParam(key)) continue; // skip page, limit, _count, ...
48
+ * // … parse as filter predicate
49
+ * }
50
+ * ```
51
+ */
52
+ function isControlParam(key) {
53
+ return STANDARD_RESERVED_PARAMS.has(key);
54
+ }
55
+ //#endregion
56
+ export { STANDARD_RESERVED_PARAMS, isControlParam };
@@ -0,0 +1,63 @@
1
+ //#region src/repository/agg-output.d.ts
2
+ /**
3
+ * Cross-kit AggResult row-shape normalization.
4
+ *
5
+ * When an `AggRequest` includes `lookups` and `groupBy` references a
6
+ * joined-alias path (e.g. `'department.code'`), the row that lands in
7
+ * `AggResult.rows` carries the joined data as a NESTED object:
8
+ *
9
+ * ```ts
10
+ * { status: 'pending', department: { code: 'ENG' }, count: 3 }
11
+ * ```
12
+ *
13
+ * Same convention `lookupPopulate` uses. Mongokit's `$project` with
14
+ * dotted-key output naturally nests; sqlitekit gets flat-dotted keys
15
+ * from Drizzle's SELECT alias map and runs results through
16
+ * `nestDottedKeys` before returning.
17
+ *
18
+ * **Why nested over flat-dotted?**
19
+ * - Matches `lookupPopulate` precedent (single convention across
20
+ * all read primitives).
21
+ * - JSON-clean: `{ department: { code: 'ENG' } }` round-trips
22
+ * identically through `JSON.stringify` / `parse`.
23
+ * - Cleaner consumer code: `row.department.code` vs
24
+ * `row['department.code']`.
25
+ * - BSON allows nested but disallows literal `.` in field names —
26
+ * the only shape that works in mongo without BSON workarounds.
27
+ *
28
+ * **Out of scope**:
29
+ * - Multi-level dotted paths (`'a.b.c'`) — kits don't emit these
30
+ * today (single-level joins only). The helper handles them
31
+ * correctly by recursive descent so future depth is supported.
32
+ * - Conflicting flat + nested keys on the same row (e.g. both
33
+ * `department` and `department.code`). The flat-dotted side wins;
34
+ * a top-level `department` value gets overwritten when a
35
+ * `department.<x>` partner key is processed. In practice this
36
+ * never happens — kits emit one or the other per groupBy key.
37
+ */
38
+ /**
39
+ * Walk a row's top-level keys, splitting any that contain `.` into
40
+ * nested objects. Keys without `.` pass through unchanged. Mutates a
41
+ * fresh output object — the input is not modified.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * nestDottedKeys({ status: 'pending', 'department.code': 'ENG', count: 3 })
46
+ * // → { status: 'pending', department: { code: 'ENG' }, count: 3 }
47
+ * ```
48
+ *
49
+ * Multi-level paths (`a.b.c`) recurse:
50
+ *
51
+ * ```ts
52
+ * nestDottedKeys({ 'a.b.c': 1 })
53
+ * // → { a: { b: { c: 1 } } }
54
+ * ```
55
+ */
56
+ declare function nestDottedKeys<T extends Record<string, unknown>>(row: T): Record<string, unknown>;
57
+ /**
58
+ * Convenience wrapper for an array of rows. Returns a new array of
59
+ * normalized rows; the input is not modified.
60
+ */
61
+ declare function nestDottedKeysAll<T extends Record<string, unknown>>(rows: readonly T[]): Record<string, unknown>[];
62
+ //#endregion
63
+ export { nestDottedKeys, nestDottedKeysAll };
@@ -0,0 +1,89 @@
1
+ //#region src/repository/agg-output.ts
2
+ /**
3
+ * Cross-kit AggResult row-shape normalization.
4
+ *
5
+ * When an `AggRequest` includes `lookups` and `groupBy` references a
6
+ * joined-alias path (e.g. `'department.code'`), the row that lands in
7
+ * `AggResult.rows` carries the joined data as a NESTED object:
8
+ *
9
+ * ```ts
10
+ * { status: 'pending', department: { code: 'ENG' }, count: 3 }
11
+ * ```
12
+ *
13
+ * Same convention `lookupPopulate` uses. Mongokit's `$project` with
14
+ * dotted-key output naturally nests; sqlitekit gets flat-dotted keys
15
+ * from Drizzle's SELECT alias map and runs results through
16
+ * `nestDottedKeys` before returning.
17
+ *
18
+ * **Why nested over flat-dotted?**
19
+ * - Matches `lookupPopulate` precedent (single convention across
20
+ * all read primitives).
21
+ * - JSON-clean: `{ department: { code: 'ENG' } }` round-trips
22
+ * identically through `JSON.stringify` / `parse`.
23
+ * - Cleaner consumer code: `row.department.code` vs
24
+ * `row['department.code']`.
25
+ * - BSON allows nested but disallows literal `.` in field names —
26
+ * the only shape that works in mongo without BSON workarounds.
27
+ *
28
+ * **Out of scope**:
29
+ * - Multi-level dotted paths (`'a.b.c'`) — kits don't emit these
30
+ * today (single-level joins only). The helper handles them
31
+ * correctly by recursive descent so future depth is supported.
32
+ * - Conflicting flat + nested keys on the same row (e.g. both
33
+ * `department` and `department.code`). The flat-dotted side wins;
34
+ * a top-level `department` value gets overwritten when a
35
+ * `department.<x>` partner key is processed. In practice this
36
+ * never happens — kits emit one or the other per groupBy key.
37
+ */
38
+ /**
39
+ * Walk a row's top-level keys, splitting any that contain `.` into
40
+ * nested objects. Keys without `.` pass through unchanged. Mutates a
41
+ * fresh output object — the input is not modified.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * nestDottedKeys({ status: 'pending', 'department.code': 'ENG', count: 3 })
46
+ * // → { status: 'pending', department: { code: 'ENG' }, count: 3 }
47
+ * ```
48
+ *
49
+ * Multi-level paths (`a.b.c`) recurse:
50
+ *
51
+ * ```ts
52
+ * nestDottedKeys({ 'a.b.c': 1 })
53
+ * // → { a: { b: { c: 1 } } }
54
+ * ```
55
+ */
56
+ function nestDottedKeys(row) {
57
+ const out = {};
58
+ for (const [key, value] of Object.entries(row)) {
59
+ if (key.indexOf(".") < 0) {
60
+ out[key] = value;
61
+ continue;
62
+ }
63
+ setDeep(out, key.split("."), value);
64
+ }
65
+ return out;
66
+ }
67
+ /**
68
+ * Convenience wrapper for an array of rows. Returns a new array of
69
+ * normalized rows; the input is not modified.
70
+ */
71
+ function nestDottedKeysAll(rows) {
72
+ return rows.map((r) => nestDottedKeys(r));
73
+ }
74
+ function setDeep(target, path, value) {
75
+ let cursor = target;
76
+ for (let i = 0; i < path.length - 1; i++) {
77
+ const segment = path[i];
78
+ const existing = cursor[segment];
79
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) cursor = existing;
80
+ else {
81
+ const next = {};
82
+ cursor[segment] = next;
83
+ cursor = next;
84
+ }
85
+ }
86
+ cursor[path[path.length - 1]] = value;
87
+ }
88
+ //#endregion
89
+ export { nestDottedKeys, nestDottedKeysAll };
@@ -15,11 +15,13 @@ var RepositoryBase = class {
15
15
  this.modelName = options.name;
16
16
  this.hooks = new HookEngine(options.hooks ?? "async");
17
17
  const plugins = options.plugins ?? [];
18
+ for (let i = 0; i < plugins.length; i++) assertValidPlugin(plugins[i], this.modelName, i);
18
19
  validatePluginOrder(plugins, this.modelName, options.pluginOrderChecks ?? "warn", options.onPluginOrderWarning);
19
20
  for (const plugin of plugins) this.use(plugin);
20
21
  }
21
22
  /** Install a plugin (object with `apply(repo)` or a plain function). */
22
23
  use(plugin) {
24
+ assertValidPlugin(plugin, this.modelName);
23
25
  if (typeof plugin === "function") plugin(this);
24
26
  else plugin.apply(this);
25
27
  return this;
@@ -107,5 +109,24 @@ var RepositoryBase = class {
107
109
  return context["_cachedResult"];
108
110
  }
109
111
  };
112
+ /**
113
+ * Reject malformed plugin entries before they reach `use()`.
114
+ *
115
+ * Caught the field-reported `new Repository(Model, ['organizationId'], ...)`
116
+ * crash where a tenant-field string array landed in the plugins slot and
117
+ * blew up with `TypeError: plugin.apply is not a function` deep inside the
118
+ * constructor. Validating shape up front turns that into a single, action-
119
+ * able error pointing at the offending index.
120
+ */
121
+ function assertValidPlugin(plugin, repoName, index) {
122
+ const where = typeof index === "number" ? `plugin at index ${index}` : "plugin";
123
+ if (plugin === null || plugin === void 0) throw new TypeError(`[repo-core] Repository "${repoName}": ${where} is ${plugin === null ? "null" : "undefined"}. Expected a function \`(repo) => void\` or an object \`{ name, apply(repo) }\`.`);
124
+ if (typeof plugin === "function") return;
125
+ if (typeof plugin !== "object") {
126
+ const detail = typeof plugin === "string" ? `'${plugin}'` : "";
127
+ throw new TypeError(`[repo-core] Repository "${repoName}": ${where} has wrong type. Expected a function or { name, apply(repo) } object — got ${typeof plugin} ${detail}. Common cause: \`new Repository(Model, [tenantField], opts)\` — second argument must be a plugins array.`);
128
+ }
129
+ if (typeof plugin.apply !== "function") throw new TypeError(`[repo-core] Repository "${repoName}": ${where} is an object but missing \`apply(repo)\`. Expected \`{ name: string, apply: (repo) => void }\`.`);
130
+ }
110
131
  //#endregion
111
132
  export { RepositoryBase };
@@ -1,6 +1,8 @@
1
1
  import { LookupPopulateOptions, LookupPopulateResult, LookupRow, LookupSpec } from "../lookup/types.mjs";
2
2
  import { UpdateInput } from "../update/types.mjs";
3
+ import { nestDottedKeys, nestDottedKeysAll } from "./agg-output.mjs";
3
4
  import { PLUGIN_ORDER_CONSTRAINTS, Plugin, PluginFunction, PluginType, validatePluginOrder } from "./plugin-types.mjs";
4
5
  import { RepositoryBase, RepositoryBaseOptions } from "./base.mjs";
5
- import { AggMeasure, AggPaginationRequest, AggRequest, AggResult, AggRow, BulkWriteOperation, BulkWriteResult, DeleteManyResult, DeleteOptions, DeleteResult, FilterInput, FindOneAndUpdateOptions, InferDoc, MinimalRepo, PaginationParams, QueryOptions, RepositorySession, StandardRepo, UpdateManyResult, WriteOptions } from "./types.mjs";
6
- export { type AggMeasure, type AggPaginationRequest, type AggRequest, type AggResult, type AggRow, type BulkWriteOperation, type BulkWriteResult, type DeleteManyResult, type DeleteOptions, type DeleteResult, type FilterInput, type FindOneAndUpdateOptions, type InferDoc, type LookupPopulateOptions, type LookupPopulateResult, type LookupRow, type LookupSpec, type MinimalRepo, PLUGIN_ORDER_CONSTRAINTS, type PaginationParams, type Plugin, type PluginFunction, type PluginType, type QueryOptions, RepositoryBase, type RepositoryBaseOptions, type RepositorySession, type StandardRepo, type UpdateInput, type UpdateManyResult, type WriteOptions, validatePluginOrder };
6
+ import { STANDARD_REPO_OPTION_KEYS, StandardRepoOptionKey } from "./options.mjs";
7
+ import { AggCacheOptions, AggDateBucket, AggDateBucketInterval, AggDateBucketUnit, AggExecutionHints, AggMeasure, AggPaginationRequest, AggRequest, AggResult, AggRow, AggTopN, AggTopNTies, BulkCreateResult, BulkWriteOperation, BulkWriteResult, ClaimTransition, ClaimVersionTransition, DeleteManyResult, DeleteOptions, DeleteResult, FilterInput, FindOneAndUpdateOptions, InferDoc, KeysetAggPaginationResult, MinimalRepo, PaginationParams, QueryOptions, RepositorySession, StandardRepo, UpdateManyResult, WriteOptions } from "./types.mjs";
8
+ export { type AggCacheOptions, type AggDateBucket, type AggDateBucketInterval, type AggDateBucketUnit, type AggExecutionHints, type AggMeasure, type AggPaginationRequest, type AggRequest, type AggResult, type AggRow, type AggTopN, type AggTopNTies, type BulkCreateResult, type BulkWriteOperation, type BulkWriteResult, type ClaimTransition, type ClaimVersionTransition, type DeleteManyResult, type DeleteOptions, type DeleteResult, type FilterInput, type FindOneAndUpdateOptions, type InferDoc, type KeysetAggPaginationResult, type LookupPopulateOptions, type LookupPopulateResult, type LookupRow, type LookupSpec, type MinimalRepo, PLUGIN_ORDER_CONSTRAINTS, type PaginationParams, type Plugin, type PluginFunction, type PluginType, type QueryOptions, RepositoryBase, type RepositoryBaseOptions, type RepositorySession, STANDARD_REPO_OPTION_KEYS, type StandardRepo, type StandardRepoOptionKey, type UpdateInput, type UpdateManyResult, type WriteOptions, nestDottedKeys, nestDottedKeysAll, validatePluginOrder };
@@ -1,3 +1,5 @@
1
+ import { nestDottedKeys, nestDottedKeysAll } from "./agg-output.mjs";
1
2
  import { PLUGIN_ORDER_CONSTRAINTS, validatePluginOrder } from "./plugin-types.mjs";
2
3
  import { RepositoryBase } from "./base.mjs";
3
- export { PLUGIN_ORDER_CONSTRAINTS, RepositoryBase, validatePluginOrder };
4
+ import { STANDARD_REPO_OPTION_KEYS } from "./options.mjs";
5
+ export { PLUGIN_ORDER_CONSTRAINTS, RepositoryBase, STANDARD_REPO_OPTION_KEYS, nestDottedKeys, nestDottedKeysAll, validatePluginOrder };
@@ -0,0 +1,62 @@
1
+ //#region src/repository/options.d.ts
2
+ /**
3
+ * Canonical option keys forwarded into every `MinimalRepo` /
4
+ * `StandardRepo` method call.
5
+ *
6
+ * The options bag is the cross-cutting plumbing every kit's plugin
7
+ * layer reads from: multi-tenant scope, audit attribution, transaction
8
+ * threading, observability correlation. Hosts (and arc-style
9
+ * frameworks) extract these from the request context once and forward
10
+ * them into every repo call so plugins don't need request-context
11
+ * access of their own.
12
+ *
13
+ * Without a single agreed-on set, drift is inevitable: one host
14
+ * forwards `userId`, another forwards `actorId`, a third forgets
15
+ * `requestId` entirely — and audit logs lose attribution silently.
16
+ * `STANDARD_REPO_OPTION_KEYS` is the contract every kit and every
17
+ * arc-style framework agrees on. Adding a key here is a deliberate
18
+ * ecosystem-wide commitment.
19
+ *
20
+ * Kits implementing custom plugins (commission, supplier-performance,
21
+ * pos, ...) can declare their own canonical sets via mongokit's
22
+ * `createOptionsExtractor<TCtx>` — that pattern stays domain-local and
23
+ * doesn't pollute the cross-kit contract.
24
+ */
25
+ /**
26
+ * The canonical keys every kit's plugin layer reads from the options
27
+ * bag, and every framework auto-threads from request context.
28
+ *
29
+ * - `organizationId` — multi-tenant scope. Tenant plugins
30
+ * (mongokit's `multiTenantPlugin`, sqlitekit's tenant filter) read
31
+ * it to stamp on write + filter on read. Cast handling (e.g.
32
+ * `ObjectId` coercion) is plugin-local — pass the raw scope id.
33
+ * - `userId` — actor id for audit attribution. Audit-log / audit-
34
+ * trail plugins read it for the `who` column.
35
+ * - `user` — denormalized actor object, when the audit log wants
36
+ * richer payload than a bare id (display name, role snapshot, ...).
37
+ * - `session` — driver-specific transaction handle. Mongoose
38
+ * `ClientSession`, better-sqlite3 transaction fn, Prisma
39
+ * transaction client. Opaque to repo-core — kits narrow at the
40
+ * boundary.
41
+ * - `requestId` — request correlation id for trace stitching across
42
+ * logs, events, and downstream service calls.
43
+ *
44
+ * Frameworks should treat this set as the canonical forward list:
45
+ * peel matching keys off the request context, drop them into the
46
+ * options bag, and let kit plugins read what they implement. Unknown
47
+ * ctx keys do NOT forward — the bag stays narrow.
48
+ */
49
+ declare const STANDARD_REPO_OPTION_KEYS: readonly ["organizationId", "userId", "user", "session", "requestId"];
50
+ /**
51
+ * Type-level union of canonical option keys. Use to constrain
52
+ * framework helpers that thread request context into repo options:
53
+ *
54
+ * ```ts
55
+ * function pickStandardOptions(ctx: Record<string, unknown>): Partial<
56
+ * Record<StandardRepoOptionKey, unknown>
57
+ * > { ... }
58
+ * ```
59
+ */
60
+ type StandardRepoOptionKey = (typeof STANDARD_REPO_OPTION_KEYS)[number];
61
+ //#endregion
62
+ export { STANDARD_REPO_OPTION_KEYS, StandardRepoOptionKey };
@@ -0,0 +1,57 @@
1
+ //#region src/repository/options.ts
2
+ /**
3
+ * Canonical option keys forwarded into every `MinimalRepo` /
4
+ * `StandardRepo` method call.
5
+ *
6
+ * The options bag is the cross-cutting plumbing every kit's plugin
7
+ * layer reads from: multi-tenant scope, audit attribution, transaction
8
+ * threading, observability correlation. Hosts (and arc-style
9
+ * frameworks) extract these from the request context once and forward
10
+ * them into every repo call so plugins don't need request-context
11
+ * access of their own.
12
+ *
13
+ * Without a single agreed-on set, drift is inevitable: one host
14
+ * forwards `userId`, another forwards `actorId`, a third forgets
15
+ * `requestId` entirely — and audit logs lose attribution silently.
16
+ * `STANDARD_REPO_OPTION_KEYS` is the contract every kit and every
17
+ * arc-style framework agrees on. Adding a key here is a deliberate
18
+ * ecosystem-wide commitment.
19
+ *
20
+ * Kits implementing custom plugins (commission, supplier-performance,
21
+ * pos, ...) can declare their own canonical sets via mongokit's
22
+ * `createOptionsExtractor<TCtx>` — that pattern stays domain-local and
23
+ * doesn't pollute the cross-kit contract.
24
+ */
25
+ /**
26
+ * The canonical keys every kit's plugin layer reads from the options
27
+ * bag, and every framework auto-threads from request context.
28
+ *
29
+ * - `organizationId` — multi-tenant scope. Tenant plugins
30
+ * (mongokit's `multiTenantPlugin`, sqlitekit's tenant filter) read
31
+ * it to stamp on write + filter on read. Cast handling (e.g.
32
+ * `ObjectId` coercion) is plugin-local — pass the raw scope id.
33
+ * - `userId` — actor id for audit attribution. Audit-log / audit-
34
+ * trail plugins read it for the `who` column.
35
+ * - `user` — denormalized actor object, when the audit log wants
36
+ * richer payload than a bare id (display name, role snapshot, ...).
37
+ * - `session` — driver-specific transaction handle. Mongoose
38
+ * `ClientSession`, better-sqlite3 transaction fn, Prisma
39
+ * transaction client. Opaque to repo-core — kits narrow at the
40
+ * boundary.
41
+ * - `requestId` — request correlation id for trace stitching across
42
+ * logs, events, and downstream service calls.
43
+ *
44
+ * Frameworks should treat this set as the canonical forward list:
45
+ * peel matching keys off the request context, drop them into the
46
+ * options bag, and let kit plugins read what they implement. Unknown
47
+ * ctx keys do NOT forward — the bag stays narrow.
48
+ */
49
+ const STANDARD_REPO_OPTION_KEYS = [
50
+ "organizationId",
51
+ "userId",
52
+ "user",
53
+ "session",
54
+ "requestId"
55
+ ];
56
+ //#endregion
57
+ export { STANDARD_REPO_OPTION_KEYS };