@classytic/repo-core 0.1.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 (84) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/LICENSE +21 -0
  3. package/README.md +154 -0
  4. package/dist/cache/index.d.mts +4 -0
  5. package/dist/cache/index.mjs +3 -0
  6. package/dist/cache/memory-adapter.d.mts +7 -0
  7. package/dist/cache/memory-adapter.mjs +37 -0
  8. package/dist/cache/stable-stringify.d.mts +15 -0
  9. package/dist/cache/stable-stringify.mjs +19 -0
  10. package/dist/cache/types.d.mts +59 -0
  11. package/dist/context/index.d.mts +2 -0
  12. package/dist/context/index.mjs +0 -0
  13. package/dist/context/types.d.mts +24 -0
  14. package/dist/errors/create-error.d.mts +19 -0
  15. package/dist/errors/create-error.mjs +23 -0
  16. package/dist/errors/duplicate-key.d.mts +38 -0
  17. package/dist/errors/duplicate-key.mjs +57 -0
  18. package/dist/errors/index.d.mts +4 -0
  19. package/dist/errors/index.mjs +3 -0
  20. package/dist/errors/types.d.mts +37 -0
  21. package/dist/filter/builders.d.mts +60 -0
  22. package/dist/filter/builders.mjs +172 -0
  23. package/dist/filter/guard.d.mts +13 -0
  24. package/dist/filter/guard.mjs +34 -0
  25. package/dist/filter/index.d.mts +7 -0
  26. package/dist/filter/index.mjs +6 -0
  27. package/dist/filter/match.d.mts +12 -0
  28. package/dist/filter/match.mjs +91 -0
  29. package/dist/filter/scope.d.mts +31 -0
  30. package/dist/filter/scope.mjs +54 -0
  31. package/dist/filter/types.d.mts +143 -0
  32. package/dist/filter/walk.d.mts +24 -0
  33. package/dist/filter/walk.mjs +77 -0
  34. package/dist/hooks/engine.d.mts +48 -0
  35. package/dist/hooks/engine.mjs +101 -0
  36. package/dist/hooks/events.d.mts +95 -0
  37. package/dist/hooks/events.mjs +93 -0
  38. package/dist/hooks/index.d.mts +5 -0
  39. package/dist/hooks/index.mjs +4 -0
  40. package/dist/hooks/priority.d.mts +23 -0
  41. package/dist/hooks/priority.mjs +21 -0
  42. package/dist/hooks/types.d.mts +37 -0
  43. package/dist/lookup/index.d.mts +2 -0
  44. package/dist/lookup/index.mjs +0 -0
  45. package/dist/lookup/types.d.mts +170 -0
  46. package/dist/operations/index.d.mts +3 -0
  47. package/dist/operations/index.mjs +2 -0
  48. package/dist/operations/registry.d.mts +41 -0
  49. package/dist/operations/registry.mjs +140 -0
  50. package/dist/operations/types.d.mts +49 -0
  51. package/dist/pagination/cursor.d.mts +44 -0
  52. package/dist/pagination/cursor.mjs +150 -0
  53. package/dist/pagination/index.d.mts +5 -0
  54. package/dist/pagination/index.mjs +4 -0
  55. package/dist/pagination/keyset.d.mts +25 -0
  56. package/dist/pagination/keyset.mjs +61 -0
  57. package/dist/pagination/offset.d.mts +26 -0
  58. package/dist/pagination/offset.mjs +47 -0
  59. package/dist/pagination/types.d.mts +136 -0
  60. package/dist/query-parser/coerce.d.mts +16 -0
  61. package/dist/query-parser/coerce.mjs +73 -0
  62. package/dist/query-parser/index.d.mts +4 -0
  63. package/dist/query-parser/index.mjs +3 -0
  64. package/dist/query-parser/parse-url.d.mts +7 -0
  65. package/dist/query-parser/parse-url.mjs +224 -0
  66. package/dist/query-parser/types.d.mts +104 -0
  67. package/dist/repository/base.d.mts +90 -0
  68. package/dist/repository/base.mjs +111 -0
  69. package/dist/repository/index.d.mts +5 -0
  70. package/dist/repository/index.mjs +3 -0
  71. package/dist/repository/plugin-types.d.mts +27 -0
  72. package/dist/repository/plugin-types.mjs +45 -0
  73. package/dist/repository/types.d.mts +470 -0
  74. package/dist/schema/field-rules.d.mts +62 -0
  75. package/dist/schema/field-rules.mjs +110 -0
  76. package/dist/schema/index.d.mts +3 -0
  77. package/dist/schema/index.mjs +2 -0
  78. package/dist/schema/types.d.mts +138 -0
  79. package/dist/testing/conformance.d.mts +6 -0
  80. package/dist/testing/conformance.mjs +481 -0
  81. package/dist/testing/index.d.mts +3 -0
  82. package/dist/testing/index.mjs +2 -0
  83. package/dist/testing/types.d.mts +113 -0
  84. package/package.json +130 -0
@@ -0,0 +1,77 @@
1
+ //#region src/filter/walk.ts
2
+ /**
3
+ * Depth-first visit of every node. Visitor returns `false` to stop descent
4
+ * into the current subtree (e.g. optimization prunes). Otherwise visits
5
+ * children.
6
+ */
7
+ function walkFilter(filter, visit) {
8
+ if (visit(filter) === false) return;
9
+ switch (filter.op) {
10
+ case "and":
11
+ case "or":
12
+ for (const child of filter.children) walkFilter(child, visit);
13
+ return;
14
+ case "not":
15
+ walkFilter(filter.child, visit);
16
+ return;
17
+ default: return;
18
+ }
19
+ }
20
+ /**
21
+ * Post-order transform. `transform` sees every node after its children have
22
+ * been rewritten, so leaf rewrites cascade up. Return the same node
23
+ * reference to opt-out of a rewrite at that level.
24
+ *
25
+ * Guarantees:
26
+ * - Immutable — never mutates input.
27
+ * - Identity-preserving — when no child changes, the parent node is
28
+ * returned unchanged (useful for structural sharing in caches).
29
+ */
30
+ function mapFilter(filter, transform) {
31
+ switch (filter.op) {
32
+ case "and": {
33
+ let changed = false;
34
+ const next = [];
35
+ for (const child of filter.children) {
36
+ const mapped = mapFilter(child, transform);
37
+ if (mapped !== child) changed = true;
38
+ next.push(mapped);
39
+ }
40
+ return transform(changed ? Object.freeze({
41
+ op: "and",
42
+ children: Object.freeze(next)
43
+ }) : filter);
44
+ }
45
+ case "or": {
46
+ let changed = false;
47
+ const next = [];
48
+ for (const child of filter.children) {
49
+ const mapped = mapFilter(child, transform);
50
+ if (mapped !== child) changed = true;
51
+ next.push(mapped);
52
+ }
53
+ return transform(changed ? Object.freeze({
54
+ op: "or",
55
+ children: Object.freeze(next)
56
+ }) : filter);
57
+ }
58
+ case "not": {
59
+ const mapped = mapFilter(filter.child, transform);
60
+ return transform(mapped === filter.child ? filter : Object.freeze({
61
+ op: "not",
62
+ child: mapped
63
+ }));
64
+ }
65
+ default: return transform(filter);
66
+ }
67
+ }
68
+ /** Collect every field name referenced in the tree. Useful for index hints, policy checks. */
69
+ function collectFields(filter) {
70
+ const seen = /* @__PURE__ */ new Set();
71
+ walkFilter(filter, (node) => {
72
+ if ("field" in node && typeof node.field === "string") seen.add(node.field);
73
+ });
74
+ return [...seen];
75
+ }
76
+ //#endregion
77
+ export { collectFields, mapFilter, walkFilter };
@@ -0,0 +1,48 @@
1
+ import { HookListener, HookMode, PrioritizedHook } from "./types.mjs";
2
+
3
+ //#region src/hooks/engine.d.ts
4
+ /** The default priority assigned when a user omits one on `on(...)`. */
5
+ declare const DEFAULT_LISTENER_PRIORITY = 500;
6
+ declare class HookEngine {
7
+ readonly mode: HookMode;
8
+ private readonly hooks;
9
+ constructor(mode?: HookMode);
10
+ /**
11
+ * Register a listener. Lower priority numbers run first. Equal priorities
12
+ * preserve registration order.
13
+ */
14
+ on(event: string, listener: HookListener, options?: {
15
+ priority?: number;
16
+ }): void;
17
+ /** Remove a specific listener. No-op if not registered. */
18
+ off(event: string, listener: HookListener): void;
19
+ /**
20
+ * Remove every listener for a specific event, or every listener entirely.
21
+ * Prefer `off(event, listener)` in plugins — blanket removal invalidates
22
+ * other plugins' hooks (the mongokit footgun the StandardRepo docs warn about).
23
+ */
24
+ removeAllListeners(event?: string): void;
25
+ /**
26
+ * Emit event synchronously — fires every listener but does NOT await.
27
+ * Async listeners that reject route their errors to `error:hook` so the
28
+ * failure isn't swallowed.
29
+ */
30
+ emit(event: string, data: unknown): void;
31
+ /** Emit event and await every listener in priority order. */
32
+ emitAsync(event: string, data: unknown): Promise<void>;
33
+ /** Emit honoring the engine's configured mode. */
34
+ emitAccordingToMode(event: string, data: unknown): Promise<void>;
35
+ /** Count listeners for an event — useful for tests. */
36
+ count(event: string): number;
37
+ /**
38
+ * Read-only snapshot of the listener registry.
39
+ *
40
+ * Returns a fresh Map whose buckets are frozen arrays, so callers can
41
+ * inspect priorities/ordering without mutating engine state. Useful for
42
+ * observability (kits often expose this as `repo._hooks` for BC with
43
+ * mongokit ≤3.9 test surface) and for debugging.
44
+ */
45
+ listeners(): Map<string, readonly PrioritizedHook[]>;
46
+ }
47
+ //#endregion
48
+ export { DEFAULT_LISTENER_PRIORITY, HookEngine };
@@ -0,0 +1,101 @@
1
+ //#region src/hooks/engine.ts
2
+ /** The default priority assigned when a user omits one on `on(...)`. */
3
+ const DEFAULT_LISTENER_PRIORITY = 500;
4
+ var HookEngine = class {
5
+ mode;
6
+ hooks;
7
+ constructor(mode = "async") {
8
+ this.mode = mode;
9
+ this.hooks = /* @__PURE__ */ new Map();
10
+ }
11
+ /**
12
+ * Register a listener. Lower priority numbers run first. Equal priorities
13
+ * preserve registration order.
14
+ */
15
+ on(event, listener, options = {}) {
16
+ const priority = options.priority ?? 500;
17
+ const bucket = this.hooks.get(event) ?? [];
18
+ bucket.push({
19
+ listener,
20
+ priority
21
+ });
22
+ bucket.sort((a, b) => a.priority - b.priority);
23
+ this.hooks.set(event, bucket);
24
+ }
25
+ /** Remove a specific listener. No-op if not registered. */
26
+ off(event, listener) {
27
+ const bucket = this.hooks.get(event);
28
+ if (!bucket) return;
29
+ const idx = bucket.findIndex((h) => h.listener === listener);
30
+ if (idx !== -1) bucket.splice(idx, 1);
31
+ }
32
+ /**
33
+ * Remove every listener for a specific event, or every listener entirely.
34
+ * Prefer `off(event, listener)` in plugins — blanket removal invalidates
35
+ * other plugins' hooks (the mongokit footgun the StandardRepo docs warn about).
36
+ */
37
+ removeAllListeners(event) {
38
+ if (event === void 0) this.hooks.clear();
39
+ else this.hooks.delete(event);
40
+ }
41
+ /**
42
+ * Emit event synchronously — fires every listener but does NOT await.
43
+ * Async listeners that reject route their errors to `error:hook` so the
44
+ * failure isn't swallowed.
45
+ */
46
+ emit(event, data) {
47
+ const bucket = this.hooks.get(event);
48
+ if (!bucket) return;
49
+ for (const { listener } of bucket) try {
50
+ const result = listener(data);
51
+ if (result && typeof result.then === "function") result.catch((err) => {
52
+ if (event === "error:hook") return;
53
+ const error = err instanceof Error ? err : new Error(String(err));
54
+ this.emit("error:hook", {
55
+ event,
56
+ error
57
+ });
58
+ });
59
+ } catch (err) {
60
+ if (event === "error:hook") continue;
61
+ const error = err instanceof Error ? err : new Error(String(err));
62
+ this.emit("error:hook", {
63
+ event,
64
+ error
65
+ });
66
+ }
67
+ }
68
+ /** Emit event and await every listener in priority order. */
69
+ async emitAsync(event, data) {
70
+ const bucket = this.hooks.get(event);
71
+ if (!bucket) return;
72
+ for (const { listener } of bucket) await listener(data);
73
+ }
74
+ /** Emit honoring the engine's configured mode. */
75
+ async emitAccordingToMode(event, data) {
76
+ if (this.mode === "async") {
77
+ await this.emitAsync(event, data);
78
+ return;
79
+ }
80
+ this.emit(event, data);
81
+ }
82
+ /** Count listeners for an event — useful for tests. */
83
+ count(event) {
84
+ return this.hooks.get(event)?.length ?? 0;
85
+ }
86
+ /**
87
+ * Read-only snapshot of the listener registry.
88
+ *
89
+ * Returns a fresh Map whose buckets are frozen arrays, so callers can
90
+ * inspect priorities/ordering without mutating engine state. Useful for
91
+ * observability (kits often expose this as `repo._hooks` for BC with
92
+ * mongokit ≤3.9 test surface) and for debugging.
93
+ */
94
+ listeners() {
95
+ const snapshot = /* @__PURE__ */ new Map();
96
+ for (const [event, bucket] of this.hooks) snapshot.set(event, Object.freeze([...bucket]));
97
+ return snapshot;
98
+ }
99
+ };
100
+ //#endregion
101
+ export { DEFAULT_LISTENER_PRIORITY, HookEngine };
@@ -0,0 +1,95 @@
1
+ //#region src/hooks/events.d.ts
2
+ /**
3
+ * Canonical repository-operation event names.
4
+ *
5
+ * Every kit's repository emits these `before:* / after:* / error:*` events
6
+ * via the hook engine. Plugin authors subscribe through `HOOK_EVENTS` so
7
+ * typos become compile errors and a plugin written against these constants
8
+ * works identically on mongokit, sqlitekit, pgkit, prismakit.
9
+ *
10
+ * ## Scope — the `MinimalRepo + StandardRepo` op set
11
+ *
12
+ * This constant covers **every op in repo-core's standard vocabulary.**
13
+ * Kits with additional native operations (mongokit's `aggregate` / `bulkWrite`,
14
+ * future pgkit's `copyFrom`, etc.) extend their own typed subset:
15
+ *
16
+ * ```ts
17
+ * export const MONGOKIT_HOOK_EVENTS = {
18
+ * ...HOOK_EVENTS,
19
+ * BEFORE_AGGREGATE: 'before:aggregate',
20
+ * AFTER_AGGREGATE: 'after:aggregate',
21
+ * ERROR_AGGREGATE: 'error:aggregate',
22
+ * } as const;
23
+ * ```
24
+ *
25
+ * Subscribing to an event that a given kit doesn't emit is a no-op, not an
26
+ * error — that's how the hook engine already behaves — so cross-kit plugins
27
+ * can safely wire listeners for the full standard set and only fire on kits
28
+ * that actually emit them.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { HOOK_EVENTS, HOOK_PRIORITY } from '@classytic/repo-core/hooks';
33
+ *
34
+ * repo.on(HOOK_EVENTS.BEFORE_CREATE, (ctx) => {
35
+ * if (!ctx.data?.organizationId) ctx.data = { ...ctx.data, organizationId: 'org_123' };
36
+ * }, { priority: HOOK_PRIORITY.POLICY });
37
+ * ```
38
+ */
39
+ declare const HOOK_EVENTS: {
40
+ readonly BEFORE_CREATE: "before:create";
41
+ readonly AFTER_CREATE: "after:create";
42
+ readonly ERROR_CREATE: "error:create";
43
+ readonly BEFORE_UPDATE: "before:update";
44
+ readonly AFTER_UPDATE: "after:update";
45
+ readonly ERROR_UPDATE: "error:update";
46
+ readonly BEFORE_DELETE: "before:delete";
47
+ readonly AFTER_DELETE: "after:delete";
48
+ readonly ERROR_DELETE: "error:delete";
49
+ readonly BEFORE_GET_BY_ID: "before:getById";
50
+ readonly AFTER_GET_BY_ID: "after:getById";
51
+ readonly ERROR_GET_BY_ID: "error:getById";
52
+ readonly BEFORE_GET_ALL: "before:getAll";
53
+ readonly AFTER_GET_ALL: "after:getAll";
54
+ readonly ERROR_GET_ALL: "error:getAll";
55
+ readonly BEFORE_CREATE_MANY: "before:createMany";
56
+ readonly AFTER_CREATE_MANY: "after:createMany";
57
+ readonly ERROR_CREATE_MANY: "error:createMany";
58
+ readonly BEFORE_UPDATE_MANY: "before:updateMany";
59
+ readonly AFTER_UPDATE_MANY: "after:updateMany";
60
+ readonly ERROR_UPDATE_MANY: "error:updateMany";
61
+ readonly BEFORE_DELETE_MANY: "before:deleteMany";
62
+ readonly AFTER_DELETE_MANY: "after:deleteMany";
63
+ readonly ERROR_DELETE_MANY: "error:deleteMany";
64
+ readonly BEFORE_FIND_ONE_AND_UPDATE: "before:findOneAndUpdate";
65
+ readonly AFTER_FIND_ONE_AND_UPDATE: "after:findOneAndUpdate";
66
+ readonly ERROR_FIND_ONE_AND_UPDATE: "error:findOneAndUpdate";
67
+ readonly BEFORE_RESTORE: "before:restore";
68
+ readonly AFTER_RESTORE: "after:restore";
69
+ readonly ERROR_RESTORE: "error:restore";
70
+ readonly BEFORE_GET_BY_QUERY: "before:getByQuery";
71
+ readonly AFTER_GET_BY_QUERY: "after:getByQuery";
72
+ readonly ERROR_GET_BY_QUERY: "error:getByQuery";
73
+ readonly BEFORE_GET_ONE: "before:getOne";
74
+ readonly AFTER_GET_ONE: "after:getOne";
75
+ readonly ERROR_GET_ONE: "error:getOne";
76
+ readonly BEFORE_FIND_ALL: "before:findAll";
77
+ readonly AFTER_FIND_ALL: "after:findAll";
78
+ readonly ERROR_FIND_ALL: "error:findAll";
79
+ readonly BEFORE_GET_OR_CREATE: "before:getOrCreate";
80
+ readonly AFTER_GET_OR_CREATE: "after:getOrCreate";
81
+ readonly ERROR_GET_OR_CREATE: "error:getOrCreate";
82
+ readonly BEFORE_COUNT: "before:count";
83
+ readonly AFTER_COUNT: "after:count";
84
+ readonly ERROR_COUNT: "error:count";
85
+ readonly BEFORE_EXISTS: "before:exists";
86
+ readonly AFTER_EXISTS: "after:exists";
87
+ readonly ERROR_EXISTS: "error:exists";
88
+ readonly BEFORE_DISTINCT: "before:distinct";
89
+ readonly AFTER_DISTINCT: "after:distinct";
90
+ readonly ERROR_DISTINCT: "error:distinct";
91
+ };
92
+ /** String-literal union of every canonical hook event name. */
93
+ type HookEventName = (typeof HOOK_EVENTS)[keyof typeof HOOK_EVENTS];
94
+ //#endregion
95
+ export { HOOK_EVENTS, HookEventName };
@@ -0,0 +1,93 @@
1
+ //#region src/hooks/events.ts
2
+ /**
3
+ * Canonical repository-operation event names.
4
+ *
5
+ * Every kit's repository emits these `before:* / after:* / error:*` events
6
+ * via the hook engine. Plugin authors subscribe through `HOOK_EVENTS` so
7
+ * typos become compile errors and a plugin written against these constants
8
+ * works identically on mongokit, sqlitekit, pgkit, prismakit.
9
+ *
10
+ * ## Scope — the `MinimalRepo + StandardRepo` op set
11
+ *
12
+ * This constant covers **every op in repo-core's standard vocabulary.**
13
+ * Kits with additional native operations (mongokit's `aggregate` / `bulkWrite`,
14
+ * future pgkit's `copyFrom`, etc.) extend their own typed subset:
15
+ *
16
+ * ```ts
17
+ * export const MONGOKIT_HOOK_EVENTS = {
18
+ * ...HOOK_EVENTS,
19
+ * BEFORE_AGGREGATE: 'before:aggregate',
20
+ * AFTER_AGGREGATE: 'after:aggregate',
21
+ * ERROR_AGGREGATE: 'error:aggregate',
22
+ * } as const;
23
+ * ```
24
+ *
25
+ * Subscribing to an event that a given kit doesn't emit is a no-op, not an
26
+ * error — that's how the hook engine already behaves — so cross-kit plugins
27
+ * can safely wire listeners for the full standard set and only fire on kits
28
+ * that actually emit them.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { HOOK_EVENTS, HOOK_PRIORITY } from '@classytic/repo-core/hooks';
33
+ *
34
+ * repo.on(HOOK_EVENTS.BEFORE_CREATE, (ctx) => {
35
+ * if (!ctx.data?.organizationId) ctx.data = { ...ctx.data, organizationId: 'org_123' };
36
+ * }, { priority: HOOK_PRIORITY.POLICY });
37
+ * ```
38
+ */
39
+ const HOOK_EVENTS = {
40
+ BEFORE_CREATE: "before:create",
41
+ AFTER_CREATE: "after:create",
42
+ ERROR_CREATE: "error:create",
43
+ BEFORE_UPDATE: "before:update",
44
+ AFTER_UPDATE: "after:update",
45
+ ERROR_UPDATE: "error:update",
46
+ BEFORE_DELETE: "before:delete",
47
+ AFTER_DELETE: "after:delete",
48
+ ERROR_DELETE: "error:delete",
49
+ BEFORE_GET_BY_ID: "before:getById",
50
+ AFTER_GET_BY_ID: "after:getById",
51
+ ERROR_GET_BY_ID: "error:getById",
52
+ BEFORE_GET_ALL: "before:getAll",
53
+ AFTER_GET_ALL: "after:getAll",
54
+ ERROR_GET_ALL: "error:getAll",
55
+ BEFORE_CREATE_MANY: "before:createMany",
56
+ AFTER_CREATE_MANY: "after:createMany",
57
+ ERROR_CREATE_MANY: "error:createMany",
58
+ BEFORE_UPDATE_MANY: "before:updateMany",
59
+ AFTER_UPDATE_MANY: "after:updateMany",
60
+ ERROR_UPDATE_MANY: "error:updateMany",
61
+ BEFORE_DELETE_MANY: "before:deleteMany",
62
+ AFTER_DELETE_MANY: "after:deleteMany",
63
+ ERROR_DELETE_MANY: "error:deleteMany",
64
+ BEFORE_FIND_ONE_AND_UPDATE: "before:findOneAndUpdate",
65
+ AFTER_FIND_ONE_AND_UPDATE: "after:findOneAndUpdate",
66
+ ERROR_FIND_ONE_AND_UPDATE: "error:findOneAndUpdate",
67
+ BEFORE_RESTORE: "before:restore",
68
+ AFTER_RESTORE: "after:restore",
69
+ ERROR_RESTORE: "error:restore",
70
+ BEFORE_GET_BY_QUERY: "before:getByQuery",
71
+ AFTER_GET_BY_QUERY: "after:getByQuery",
72
+ ERROR_GET_BY_QUERY: "error:getByQuery",
73
+ BEFORE_GET_ONE: "before:getOne",
74
+ AFTER_GET_ONE: "after:getOne",
75
+ ERROR_GET_ONE: "error:getOne",
76
+ BEFORE_FIND_ALL: "before:findAll",
77
+ AFTER_FIND_ALL: "after:findAll",
78
+ ERROR_FIND_ALL: "error:findAll",
79
+ BEFORE_GET_OR_CREATE: "before:getOrCreate",
80
+ AFTER_GET_OR_CREATE: "after:getOrCreate",
81
+ ERROR_GET_OR_CREATE: "error:getOrCreate",
82
+ BEFORE_COUNT: "before:count",
83
+ AFTER_COUNT: "after:count",
84
+ ERROR_COUNT: "error:count",
85
+ BEFORE_EXISTS: "before:exists",
86
+ AFTER_EXISTS: "after:exists",
87
+ ERROR_EXISTS: "error:exists",
88
+ BEFORE_DISTINCT: "before:distinct",
89
+ AFTER_DISTINCT: "after:distinct",
90
+ ERROR_DISTINCT: "error:distinct"
91
+ };
92
+ //#endregion
93
+ export { HOOK_EVENTS };
@@ -0,0 +1,5 @@
1
+ import { EventPhase, HookListener, HookMode, PrioritizedHook } from "./types.mjs";
2
+ import { DEFAULT_LISTENER_PRIORITY, HookEngine } from "./engine.mjs";
3
+ import { HOOK_EVENTS, HookEventName } from "./events.mjs";
4
+ import { HOOK_PRIORITY, HookPriority } from "./priority.mjs";
5
+ export { DEFAULT_LISTENER_PRIORITY, type EventPhase, HOOK_EVENTS, HOOK_PRIORITY, HookEngine, type HookEventName, type HookListener, type HookMode, type HookPriority, type PrioritizedHook };
@@ -0,0 +1,4 @@
1
+ import { DEFAULT_LISTENER_PRIORITY, HookEngine } from "./engine.mjs";
2
+ import { HOOK_EVENTS } from "./events.mjs";
3
+ import { HOOK_PRIORITY } from "./priority.mjs";
4
+ export { DEFAULT_LISTENER_PRIORITY, HOOK_EVENTS, HOOK_PRIORITY, HookEngine };
@@ -0,0 +1,23 @@
1
+ //#region src/hooks/priority.d.ts
2
+ /**
3
+ * Hook priority constants for the repository lifecycle.
4
+ *
5
+ * Lower numbers run first. Policy hooks (multi-tenant scope, soft-delete
6
+ * filtering, validation) must run before cache lookup so that tenant and
7
+ * deletion filters are part of the cache key — otherwise one tenant can
8
+ * read another tenant's cached row.
9
+ *
10
+ * Driver-agnostic: both `@classytic/mongokit` and future kits
11
+ * (`pgkit`, `prismakit`) use these same priorities so cross-kit plugins
12
+ * compose identically.
13
+ */
14
+ declare const HOOK_PRIORITY: {
15
+ /** Policy enforcement — tenant isolation, soft-delete filtering, validation. */readonly POLICY: 100; /** Cache lookup / store — must run after policy so filters are in the key. */
16
+ readonly CACHE: 200; /** Observability — audit logging, metrics, telemetry. Must not mutate context. */
17
+ readonly OBSERVABILITY: 300; /** Default priority for user-registered hooks with no explicit priority. */
18
+ readonly DEFAULT: 500;
19
+ };
20
+ /** The numeric type of any `HOOK_PRIORITY` value. */
21
+ type HookPriority = (typeof HOOK_PRIORITY)[keyof typeof HOOK_PRIORITY];
22
+ //#endregion
23
+ export { HOOK_PRIORITY, HookPriority };
@@ -0,0 +1,21 @@
1
+ //#region src/hooks/priority.ts
2
+ /**
3
+ * Hook priority constants for the repository lifecycle.
4
+ *
5
+ * Lower numbers run first. Policy hooks (multi-tenant scope, soft-delete
6
+ * filtering, validation) must run before cache lookup so that tenant and
7
+ * deletion filters are part of the cache key — otherwise one tenant can
8
+ * read another tenant's cached row.
9
+ *
10
+ * Driver-agnostic: both `@classytic/mongokit` and future kits
11
+ * (`pgkit`, `prismakit`) use these same priorities so cross-kit plugins
12
+ * compose identically.
13
+ */
14
+ const HOOK_PRIORITY = {
15
+ POLICY: 100,
16
+ CACHE: 200,
17
+ OBSERVABILITY: 300,
18
+ DEFAULT: 500
19
+ };
20
+ //#endregion
21
+ export { HOOK_PRIORITY };
@@ -0,0 +1,37 @@
1
+ //#region src/hooks/types.d.ts
2
+ /**
3
+ * Hook system types.
4
+ *
5
+ * The hook engine sits on every `RepositoryBase` instance. Plugins register
6
+ * `before:op` / `after:op` / `error:op` listeners with a priority; the
7
+ * engine runs them in sorted order. Priorities are the coordination
8
+ * mechanism that lets multi-tenant scope inject before cache lookup (so
9
+ * tenant ID makes it into the cache key) — see `HOOK_PRIORITY`.
10
+ */
11
+ /**
12
+ * Hook listener signature. The data argument's shape depends on the event
13
+ * phase:
14
+ * - `before:*` → `RepositoryContext` (mutate to inject filters, data, etc.)
15
+ * - `after:*` → `{ context, result }`
16
+ * - `error:*` → `{ context, error }`
17
+ *
18
+ * Listeners may be sync or async. The engine awaits async listeners in
19
+ * priority order.
20
+ */
21
+ type HookListener<TData = unknown> = (data: TData) => void | Promise<void>;
22
+ /**
23
+ * Execution mode for event emission. `async` awaits every listener before
24
+ * returning (default — plugins rely on this to mutate context synchronously
25
+ * from the caller's perspective). `sync` runs listeners but doesn't await;
26
+ * fire-and-forget telemetry can use it.
27
+ */
28
+ type HookMode = 'async' | 'sync';
29
+ /** A hook entry in the priority-sorted registry. */
30
+ interface PrioritizedHook {
31
+ readonly listener: HookListener;
32
+ readonly priority: number;
33
+ }
34
+ /** Event phase discriminator for hook names. */
35
+ type EventPhase = 'before' | 'after' | 'error';
36
+ //#endregion
37
+ export { EventPhase, HookListener, HookMode, PrioritizedHook };
@@ -0,0 +1,2 @@
1
+ import { LookupPopulateOptions, LookupPopulateResult, LookupRow, LookupSpec } from "./types.mjs";
2
+ export { type LookupPopulateOptions, type LookupPopulateResult, type LookupRow, type LookupSpec };
File without changes