@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,104 @@
1
+ import { Filter } from "../filter/types.mjs";
2
+
3
+ //#region src/query-parser/types.d.ts
4
+ /** Sort direction on a single field. */
5
+ type ParsedSortDirection = 1 | -1;
6
+ /** Sort spec after parsing — array so field order is preserved. */
7
+ type ParsedSort = Record<string, ParsedSortDirection>;
8
+ /** Projection — field inclusion/exclusion map (`1` include, `0` exclude). */
9
+ type ParsedSelect = Record<string, 0 | 1>;
10
+ /**
11
+ * Populate / include spec for relation fetching. Kits interpret per backend:
12
+ *
13
+ * - mongokit compiles to Mongoose `populate()`
14
+ * - sqlitekit uses it as a hint for JOIN generation (future)
15
+ * - prismakit compiles to `include: {...}`
16
+ *
17
+ * Frontends pass it through from URL `populate[field][select]=...`
18
+ * params; the shape matches mongoose's `PopulateOptions`.
19
+ */
20
+ interface ParsedPopulate {
21
+ path: string;
22
+ select?: string;
23
+ match?: Record<string, unknown>;
24
+ options?: {
25
+ limit?: number;
26
+ sort?: ParsedSort;
27
+ skip?: number;
28
+ };
29
+ populate?: ParsedPopulate;
30
+ }
31
+ /**
32
+ * Canonical parsed-query envelope. Every kit receives this shape, every
33
+ * frontend emits URLs that produce it, arc's BaseController threads it
34
+ * into repo calls. **Do not add kit-specific fields here** — kits extend
35
+ * their own options types for native-only features.
36
+ */
37
+ interface ParsedQuery {
38
+ /** Filter IR tree. Always present (TRUE when no filter params). */
39
+ filter: Filter;
40
+ /** Optional sort spec. When absent, kits apply their default sort. */
41
+ sort?: ParsedSort;
42
+ /** Field projection. */
43
+ select?: ParsedSelect;
44
+ /** Relation population (when the kit supports it). */
45
+ populate?: ParsedPopulate[];
46
+ /** 1-indexed page. Present only when the URL used offset-pagination params. */
47
+ page?: number;
48
+ /** Opaque cursor from a prior `next`. Present only on keyset requests. */
49
+ after?: string;
50
+ /** Per-page item count. */
51
+ limit: number;
52
+ /** Free-text search term (kits interpret per backend — $text, FTS, etc.). */
53
+ search?: string;
54
+ }
55
+ /**
56
+ * Configuration knobs for the parser. All optional — sane defaults cover
57
+ * typical arc use cases.
58
+ */
59
+ interface QueryParserOptions {
60
+ /** Default per-page count when the URL omits `limit`. Default: 20. */
61
+ defaultLimit?: number;
62
+ /** Hard cap on `limit` to prevent resource exhaustion. Default: 200. */
63
+ maxLimit?: number;
64
+ /** Allowlist of filter field names. When set, unknown fields are dropped. */
65
+ allowedFilterFields?: readonly string[];
66
+ /** Allowlist of sort field names. When set, unknown fields are dropped. */
67
+ allowedSortFields?: readonly string[];
68
+ /** Allowlist of operator names accepted in bracket syntax. */
69
+ allowedOperators?: readonly BracketOperator[];
70
+ /** Max filter nesting depth (defends against filter-bomb URLs). Default: 10. */
71
+ maxFilterDepth?: number;
72
+ /** Regex pattern length cap (ReDoS defense). Default: 500. */
73
+ maxRegexLength?: number;
74
+ /** Search query length cap. Default: 200. */
75
+ maxSearchLength?: number;
76
+ /**
77
+ * Field-type hints for value coercion. When a filter field is in this
78
+ * map, the URL string value is coerced to the declared type:
79
+ * - `'number'` → `Number(value)`
80
+ * - `'boolean'` → `'true'`/`'1'` → true, else false
81
+ * - `'date'` → `new Date(value)` (validated)
82
+ * - `'string'` → left as-is (default for unlisted fields)
83
+ *
84
+ * Use this to avoid the heuristic coercion's footguns (e.g. `?sku=12345`
85
+ * against a string SKU column — without a hint, heuristics turn it into
86
+ * a number and the SQL comparison fails).
87
+ */
88
+ fieldTypes?: Record<string, 'string' | 'number' | 'boolean' | 'date'>;
89
+ }
90
+ /**
91
+ * Bracket operators accepted in URL syntax. When you see `field[op]=value`
92
+ * in a URL, `op` is one of these. Kept as a closed set — new operators
93
+ * require a new version.
94
+ */
95
+ type BracketOperator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'like' | 'contains' | 'startsWith' | 'endsWith' | 'ieq' | 'regex' | 'between' | 'exists';
96
+ /**
97
+ * Input shape for `parseUrl`. URL search params can be sourced from
98
+ * `URLSearchParams.entries()`, Fastify's `request.query`, or
99
+ * Express's `req.query` — all of these produce a `Record<string, string | string[]>`
100
+ * or a compatible iterable. The parser normalizes.
101
+ */
102
+ type QueryParserInput = URLSearchParams | Record<string, string | string[] | undefined> | Iterable<[string, string]>;
103
+ //#endregion
104
+ export { BracketOperator, ParsedPopulate, ParsedQuery, ParsedSelect, ParsedSort, ParsedSortDirection, QueryParserInput, QueryParserOptions };
@@ -0,0 +1,90 @@
1
+ import { RepositoryContext } from "../context/types.mjs";
2
+ import { HookListener, HookMode } from "../hooks/types.mjs";
3
+ import { HookEngine } from "../hooks/engine.mjs";
4
+ import { PluginType } from "./plugin-types.mjs";
5
+
6
+ //#region src/repository/base.d.ts
7
+ /** Construction options common to every kit. */
8
+ interface RepositoryBaseOptions {
9
+ /** Stable identifier (model/table name) — surfaces in hook contexts and ordering errors. */
10
+ name: string;
11
+ /** Plugins to install at construction. Order matters — see `PLUGIN_ORDER_CONSTRAINTS`. */
12
+ plugins?: readonly PluginType[];
13
+ /** Emit mode for the hook engine. Default: `'async'`. */
14
+ hooks?: HookMode;
15
+ /** Plugin-order validation mode. Default: `'warn'`. */
16
+ pluginOrderChecks?: 'warn' | 'throw' | 'off';
17
+ /** Optional callback for plugin-order warnings (defaults to `console.warn`). */
18
+ onPluginOrderWarning?: (message: string) => void;
19
+ }
20
+ /**
21
+ * Base class every driver kit extends. Exposes the hook surface
22
+ * (`on` / `off` / `emit` / `emitAsync`), plugin installation (`use`), and
23
+ * context building (`_buildContext`). Kits layer their CRUD on top.
24
+ */
25
+ declare abstract class RepositoryBase {
26
+ /** Model/table identifier — set once at construction, read on every hook context. */
27
+ readonly modelName: string;
28
+ /** Public hook engine so plugins + kits dispatch directly without a method wrapper. */
29
+ readonly hooks: HookEngine;
30
+ /**
31
+ * Open-ended property bag so plugins can install methods on the repo
32
+ * (e.g. `softDeletePlugin` adds `repo.restore(id)`). The `unknown` escape
33
+ * hatch keeps the class assignable to the richer `StandardRepo` contract
34
+ * without casts.
35
+ */
36
+ [key: string]: unknown;
37
+ constructor(options: RepositoryBaseOptions);
38
+ /** Install a plugin (object with `apply(repo)` or a plain function). */
39
+ use(plugin: PluginType): this;
40
+ /**
41
+ * Register a hook listener. Lower priority numbers run first.
42
+ * Generic over listener data type so plugins can annotate their handler
43
+ * parameter (e.g. `(ctx: RepositoryContext) => void`) without casting.
44
+ */
45
+ on<TData = unknown>(event: string, listener: HookListener<TData>, options?: {
46
+ priority?: number;
47
+ }): this;
48
+ /** Remove a specific listener. */
49
+ off<TData = unknown>(event: string, listener: HookListener<TData>): this;
50
+ /** Remove all listeners for an event (or all events when omitted). */
51
+ removeAllListeners(event?: string): this;
52
+ /** Emit fire-and-forget (async errors routed to `error:hook`). */
53
+ emit(event: string, data: unknown): void;
54
+ /** Emit and await every listener in priority order. */
55
+ emitAsync(event: string, data: unknown): Promise<void>;
56
+ /**
57
+ * Build a context + run before-hooks. Kits call this at the top of every
58
+ * op, mutate as needed (e.g. compile the filter after plugins have injected
59
+ * scope), perform the native operation, then call `_emitAfter`.
60
+ *
61
+ * **Always awaits before-hooks** regardless of the engine's `hooks` mode.
62
+ * The before-phase is where policy plugins (multi-tenant, soft-delete,
63
+ * validation) inject scope and reject requests — fire-and-forget here
64
+ * would let the driver call run before the filter is augmented. `hooks:
65
+ * 'sync'` only affects `_emitAfter` / `_emitError`, where fire-and-forget
66
+ * is acceptable for observability/metrics listeners.
67
+ */
68
+ _buildContext<TOptions extends Record<string, unknown>>(operation: string, inputs: TOptions): Promise<RepositoryContext>;
69
+ /** Emit after-hook. Kits invoke this after the native op succeeds. */
70
+ _emitAfter(operation: string, context: RepositoryContext, result: unknown): Promise<void>;
71
+ /**
72
+ * Emit error-hook. Swallows any throw from within the hook itself so the
73
+ * original operation's error remains authoritative.
74
+ */
75
+ _emitError(operation: string, context: RepositoryContext, error: Error): Promise<void>;
76
+ /**
77
+ * Cache-plugin escape hatch. When the cache plugin's `before:*` hook
78
+ * finds a hit, it stamps `_cacheHit = true` + `_cachedResult` onto the
79
+ * context. Kits call this helper at the top of every read op right after
80
+ * `_buildContext`. Returns the cached value, or `undefined` when there
81
+ * was no hit (kit proceeds with the native driver call).
82
+ *
83
+ * Returns `undefined` also covers "hit, but the cached value was undefined" —
84
+ * callers typically treat that as a miss since repo methods return null,
85
+ * not undefined, for "not found".
86
+ */
87
+ _cachedValue<T>(context: RepositoryContext): T | undefined;
88
+ }
89
+ //#endregion
90
+ export { RepositoryBase, RepositoryBaseOptions };
@@ -0,0 +1,111 @@
1
+ import { HookEngine } from "../hooks/engine.mjs";
2
+ import { validatePluginOrder } from "./plugin-types.mjs";
3
+ //#region src/repository/base.ts
4
+ /**
5
+ * Base class every driver kit extends. Exposes the hook surface
6
+ * (`on` / `off` / `emit` / `emitAsync`), plugin installation (`use`), and
7
+ * context building (`_buildContext`). Kits layer their CRUD on top.
8
+ */
9
+ var RepositoryBase = class {
10
+ /** Model/table identifier — set once at construction, read on every hook context. */
11
+ modelName;
12
+ /** Public hook engine so plugins + kits dispatch directly without a method wrapper. */
13
+ hooks;
14
+ constructor(options) {
15
+ this.modelName = options.name;
16
+ this.hooks = new HookEngine(options.hooks ?? "async");
17
+ const plugins = options.plugins ?? [];
18
+ validatePluginOrder(plugins, this.modelName, options.pluginOrderChecks ?? "warn", options.onPluginOrderWarning);
19
+ for (const plugin of plugins) this.use(plugin);
20
+ }
21
+ /** Install a plugin (object with `apply(repo)` or a plain function). */
22
+ use(plugin) {
23
+ if (typeof plugin === "function") plugin(this);
24
+ else plugin.apply(this);
25
+ return this;
26
+ }
27
+ /**
28
+ * Register a hook listener. Lower priority numbers run first.
29
+ * Generic over listener data type so plugins can annotate their handler
30
+ * parameter (e.g. `(ctx: RepositoryContext) => void`) without casting.
31
+ */
32
+ on(event, listener, options) {
33
+ this.hooks.on(event, listener, options);
34
+ return this;
35
+ }
36
+ /** Remove a specific listener. */
37
+ off(event, listener) {
38
+ this.hooks.off(event, listener);
39
+ return this;
40
+ }
41
+ /** Remove all listeners for an event (or all events when omitted). */
42
+ removeAllListeners(event) {
43
+ this.hooks.removeAllListeners(event);
44
+ return this;
45
+ }
46
+ /** Emit fire-and-forget (async errors routed to `error:hook`). */
47
+ emit(event, data) {
48
+ this.hooks.emit(event, data);
49
+ }
50
+ /** Emit and await every listener in priority order. */
51
+ async emitAsync(event, data) {
52
+ await this.hooks.emitAsync(event, data);
53
+ }
54
+ /**
55
+ * Build a context + run before-hooks. Kits call this at the top of every
56
+ * op, mutate as needed (e.g. compile the filter after plugins have injected
57
+ * scope), perform the native operation, then call `_emitAfter`.
58
+ *
59
+ * **Always awaits before-hooks** regardless of the engine's `hooks` mode.
60
+ * The before-phase is where policy plugins (multi-tenant, soft-delete,
61
+ * validation) inject scope and reject requests — fire-and-forget here
62
+ * would let the driver call run before the filter is augmented. `hooks:
63
+ * 'sync'` only affects `_emitAfter` / `_emitError`, where fire-and-forget
64
+ * is acceptable for observability/metrics listeners.
65
+ */
66
+ async _buildContext(operation, inputs) {
67
+ const context = {
68
+ operation,
69
+ model: this.modelName,
70
+ ...inputs
71
+ };
72
+ await this.hooks.emitAsync(`before:${operation}`, context);
73
+ return context;
74
+ }
75
+ /** Emit after-hook. Kits invoke this after the native op succeeds. */
76
+ async _emitAfter(operation, context, result) {
77
+ await this.hooks.emitAccordingToMode(`after:${operation}`, {
78
+ context,
79
+ result
80
+ });
81
+ }
82
+ /**
83
+ * Emit error-hook. Swallows any throw from within the hook itself so the
84
+ * original operation's error remains authoritative.
85
+ */
86
+ async _emitError(operation, context, error) {
87
+ try {
88
+ await this.hooks.emitAccordingToMode(`error:${operation}`, {
89
+ context,
90
+ error
91
+ });
92
+ } catch {}
93
+ }
94
+ /**
95
+ * Cache-plugin escape hatch. When the cache plugin's `before:*` hook
96
+ * finds a hit, it stamps `_cacheHit = true` + `_cachedResult` onto the
97
+ * context. Kits call this helper at the top of every read op right after
98
+ * `_buildContext`. Returns the cached value, or `undefined` when there
99
+ * was no hit (kit proceeds with the native driver call).
100
+ *
101
+ * Returns `undefined` also covers "hit, but the cached value was undefined" —
102
+ * callers typically treat that as a miss since repo methods return null,
103
+ * not undefined, for "not found".
104
+ */
105
+ _cachedValue(context) {
106
+ if (context["_cacheHit"] !== true) return void 0;
107
+ return context["_cachedResult"];
108
+ }
109
+ };
110
+ //#endregion
111
+ export { RepositoryBase };
@@ -0,0 +1,5 @@
1
+ import { LookupPopulateOptions, LookupPopulateResult, LookupRow, LookupSpec } from "../lookup/types.mjs";
2
+ import { PLUGIN_ORDER_CONSTRAINTS, Plugin, PluginFunction, PluginType, validatePluginOrder } from "./plugin-types.mjs";
3
+ import { RepositoryBase, RepositoryBaseOptions } from "./base.mjs";
4
+ import { AggMeasure, AggPaginationRequest, AggRequest, AggResult, AggRow, BulkWriteOperation, BulkWriteResult, DeleteManyResult, DeleteOptions, DeleteResult, FindOneAndUpdateOptions, InferDoc, MinimalRepo, PaginationParams, QueryOptions, RepositorySession, StandardRepo, UpdateManyResult, WriteOptions } from "./types.mjs";
5
+ export { type AggMeasure, type AggPaginationRequest, type AggRequest, type AggResult, type AggRow, type BulkWriteOperation, type BulkWriteResult, type DeleteManyResult, type DeleteOptions, type DeleteResult, 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 UpdateManyResult, type WriteOptions, validatePluginOrder };
@@ -0,0 +1,3 @@
1
+ import { PLUGIN_ORDER_CONSTRAINTS, validatePluginOrder } from "./plugin-types.mjs";
2
+ import { RepositoryBase } from "./base.mjs";
3
+ export { PLUGIN_ORDER_CONSTRAINTS, RepositoryBase, validatePluginOrder };
@@ -0,0 +1,27 @@
1
+ import { RepositoryBase } from "./base.mjs";
2
+
3
+ //#region src/repository/plugin-types.d.ts
4
+ /** Object-style plugin — preferred form. Carries a stable `name` for ordering checks. */
5
+ interface Plugin<TRepo extends RepositoryBase = RepositoryBase> {
6
+ /** Stable identifier used by plugin-order validators. */
7
+ readonly name: string;
8
+ apply(repo: TRepo): void;
9
+ }
10
+ /** Function-style plugin for quick-and-dirty one-off extensions. No ordering guarantees. */
11
+ type PluginFunction<TRepo extends RepositoryBase = RepositoryBase> = (repo: TRepo) => void;
12
+ /** Either flavor is accepted by `repo.use(...)`. */
13
+ type PluginType<TRepo extends RepositoryBase = RepositoryBase> = Plugin<TRepo> | PluginFunction<TRepo>;
14
+ /**
15
+ * Ordered pairs that produce wrong behavior when installed out of order.
16
+ * Each entry: `[mustComeFirst, mustComeAfter, reason]`.
17
+ *
18
+ * Surfaced by `validatePluginOrder` at repository construction time.
19
+ */
20
+ declare const PLUGIN_ORDER_CONSTRAINTS: readonly (readonly [string, string, string])[];
21
+ /**
22
+ * Assert plugin-install order. Emits a `warn` (default) or throws based on
23
+ * mode. Plain-function plugins (no `name`) are skipped — no false positives.
24
+ */
25
+ declare function validatePluginOrder(plugins: readonly PluginType[], repoName: string, mode?: 'warn' | 'throw' | 'off', onWarn?: (message: string) => void): void;
26
+ //#endregion
27
+ export { PLUGIN_ORDER_CONSTRAINTS, Plugin, PluginFunction, PluginType, validatePluginOrder };
@@ -0,0 +1,45 @@
1
+ //#region src/repository/plugin-types.ts
2
+ /**
3
+ * Ordered pairs that produce wrong behavior when installed out of order.
4
+ * Each entry: `[mustComeFirst, mustComeAfter, reason]`.
5
+ *
6
+ * Surfaced by `validatePluginOrder` at repository construction time.
7
+ */
8
+ const PLUGIN_ORDER_CONSTRAINTS = [
9
+ [
10
+ "soft-delete",
11
+ "batch-operations",
12
+ "soft-delete must precede batch-operations so bulk deletes/updates see the soft-delete filter"
13
+ ],
14
+ [
15
+ "multi-tenant",
16
+ "cache",
17
+ "multi-tenant must precede cache so tenant scoping is baked into cache keys (prevents cross-tenant cache poisoning)"
18
+ ],
19
+ [
20
+ "multi-tenant",
21
+ "soft-delete",
22
+ "multi-tenant should precede soft-delete so tenant scope is applied before deletion-state filter"
23
+ ]
24
+ ];
25
+ /**
26
+ * Assert plugin-install order. Emits a `warn` (default) or throws based on
27
+ * mode. Plain-function plugins (no `name`) are skipped — no false positives.
28
+ */
29
+ function validatePluginOrder(plugins, repoName, mode = "warn", onWarn = (m) => {
30
+ console.warn(m);
31
+ }) {
32
+ if (mode === "off") return;
33
+ const names = plugins.map((p) => typeof p === "function" ? void 0 : p.name);
34
+ for (const [first, after, reason] of PLUGIN_ORDER_CONSTRAINTS) {
35
+ const firstIdx = names.indexOf(first);
36
+ const afterIdx = names.indexOf(after);
37
+ if (firstIdx === -1 || afterIdx === -1) continue;
38
+ if (firstIdx < afterIdx) continue;
39
+ const message = `[repo-core] Repository "${repoName}": plugin order issue — ${reason}. Got: [..., '${after}' at index ${String(afterIdx)}, '${first}' at index ${String(firstIdx)}]. Swap them, or pass { pluginOrderChecks: 'off' } to silence.`;
40
+ if (mode === "throw") throw new Error(message);
41
+ onWarn(message);
42
+ }
43
+ }
44
+ //#endregion
45
+ export { PLUGIN_ORDER_CONSTRAINTS, validatePluginOrder };