@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.
- package/CHANGELOG.md +67 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/dist/cache/index.d.mts +4 -0
- package/dist/cache/index.mjs +3 -0
- package/dist/cache/memory-adapter.d.mts +7 -0
- package/dist/cache/memory-adapter.mjs +37 -0
- package/dist/cache/stable-stringify.d.mts +15 -0
- package/dist/cache/stable-stringify.mjs +19 -0
- package/dist/cache/types.d.mts +59 -0
- package/dist/context/index.d.mts +2 -0
- package/dist/context/index.mjs +0 -0
- package/dist/context/types.d.mts +24 -0
- package/dist/errors/create-error.d.mts +19 -0
- package/dist/errors/create-error.mjs +23 -0
- package/dist/errors/duplicate-key.d.mts +38 -0
- package/dist/errors/duplicate-key.mjs +57 -0
- package/dist/errors/index.d.mts +4 -0
- package/dist/errors/index.mjs +3 -0
- package/dist/errors/types.d.mts +37 -0
- package/dist/filter/builders.d.mts +60 -0
- package/dist/filter/builders.mjs +172 -0
- package/dist/filter/guard.d.mts +13 -0
- package/dist/filter/guard.mjs +34 -0
- package/dist/filter/index.d.mts +7 -0
- package/dist/filter/index.mjs +6 -0
- package/dist/filter/match.d.mts +12 -0
- package/dist/filter/match.mjs +91 -0
- package/dist/filter/scope.d.mts +31 -0
- package/dist/filter/scope.mjs +54 -0
- package/dist/filter/types.d.mts +143 -0
- package/dist/filter/walk.d.mts +24 -0
- package/dist/filter/walk.mjs +77 -0
- package/dist/hooks/engine.d.mts +48 -0
- package/dist/hooks/engine.mjs +101 -0
- package/dist/hooks/events.d.mts +95 -0
- package/dist/hooks/events.mjs +93 -0
- package/dist/hooks/index.d.mts +5 -0
- package/dist/hooks/index.mjs +4 -0
- package/dist/hooks/priority.d.mts +23 -0
- package/dist/hooks/priority.mjs +21 -0
- package/dist/hooks/types.d.mts +37 -0
- package/dist/lookup/index.d.mts +2 -0
- package/dist/lookup/index.mjs +0 -0
- package/dist/lookup/types.d.mts +170 -0
- package/dist/operations/index.d.mts +3 -0
- package/dist/operations/index.mjs +2 -0
- package/dist/operations/registry.d.mts +41 -0
- package/dist/operations/registry.mjs +140 -0
- package/dist/operations/types.d.mts +49 -0
- package/dist/pagination/cursor.d.mts +44 -0
- package/dist/pagination/cursor.mjs +150 -0
- package/dist/pagination/index.d.mts +5 -0
- package/dist/pagination/index.mjs +4 -0
- package/dist/pagination/keyset.d.mts +25 -0
- package/dist/pagination/keyset.mjs +61 -0
- package/dist/pagination/offset.d.mts +26 -0
- package/dist/pagination/offset.mjs +47 -0
- package/dist/pagination/types.d.mts +136 -0
- package/dist/query-parser/coerce.d.mts +16 -0
- package/dist/query-parser/coerce.mjs +73 -0
- package/dist/query-parser/index.d.mts +4 -0
- package/dist/query-parser/index.mjs +3 -0
- package/dist/query-parser/parse-url.d.mts +7 -0
- package/dist/query-parser/parse-url.mjs +224 -0
- package/dist/query-parser/types.d.mts +104 -0
- package/dist/repository/base.d.mts +90 -0
- package/dist/repository/base.mjs +111 -0
- package/dist/repository/index.d.mts +5 -0
- package/dist/repository/index.mjs +3 -0
- package/dist/repository/plugin-types.d.mts +27 -0
- package/dist/repository/plugin-types.mjs +45 -0
- package/dist/repository/types.d.mts +470 -0
- package/dist/schema/field-rules.d.mts +62 -0
- package/dist/schema/field-rules.mjs +110 -0
- package/dist/schema/index.d.mts +3 -0
- package/dist/schema/index.mjs +2 -0
- package/dist/schema/types.d.mts +138 -0
- package/dist/testing/conformance.d.mts +6 -0
- package/dist/testing/conformance.mjs +481 -0
- package/dist/testing/index.d.mts +3 -0
- package/dist/testing/index.mjs +2 -0
- package/dist/testing/types.d.mts +113 -0
- 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,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 };
|