@classytic/repo-core 0.17.0 → 0.18.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 +37 -0
- package/dist/cleanup/define-purge-step.d.mts +76 -0
- package/dist/cleanup/define-purge-step.mjs +107 -0
- package/dist/cleanup/index.d.mts +2 -1
- package/dist/cleanup/index.mjs +2 -0
- package/dist/tenant/index.d.mts +2 -2
- package/dist/tenant/index.mjs +2 -2
- package/dist/tenant/resolve.d.mts +17 -1
- package/dist/tenant/resolve.mjs +21 -1
- package/dist/testing/index.d.mts +1 -1
- package/dist/testing/index.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ All notable changes to `@classytic/repo-core` are documented here.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.18.0] - 2026-07-27
|
|
8
|
+
|
|
9
|
+
### Added — `definePurgeStep` builder (`./cleanup`) and `resolveTenantField` (`./tenant`)
|
|
10
|
+
|
|
11
|
+
#### `./cleanup` — `definePurgeStep`
|
|
12
|
+
|
|
13
|
+
- **`definePurgeStep(repository, spec)`** — standard builder for the most common
|
|
14
|
+
`CleanupStep` shape: a chunked `purgeByField` over a single scope value.
|
|
15
|
+
Nearly every provider step in a domain kernel is this pattern; the builder owns
|
|
16
|
+
the invariant core so callers only declare what differs:
|
|
17
|
+
- **fail-closed scoping** — missing scope value or unavailable repository is a
|
|
18
|
+
`BLOCKER` (not a silent no-op and never an unscoped purge that would hit every
|
|
19
|
+
tenant); blocked by `SCOPE_REQUIRED:<param>` / `REPOSITORY_UNAVAILABLE:<id>`.
|
|
20
|
+
- **cancellation** — `throwIfCancelled` is called before work starts and the
|
|
21
|
+
`signal` is threaded to the kit so a cancel lands between committed chunks.
|
|
22
|
+
- **honest failure** — a failing purge returns `ok: false` so the recipe composer
|
|
23
|
+
stops (retention §8); thrown errors are caught and reported, never swallowed.
|
|
24
|
+
- **verification** — absence is re-queried after the run via `countDocuments`
|
|
25
|
+
(`spec.verifyFilter` for steps whose match filter differs from the absence proof).
|
|
26
|
+
- **`PurgeStepSpec`** — declaration object: `id`, `resource`, `parameter`, `field`,
|
|
27
|
+
`strategy`, optional `retained`, `warnings`, `batchSize`, `guard(scope, ctx)`,
|
|
28
|
+
`verifyFilter(scope)`, `verifyName`.
|
|
29
|
+
- **`PurgeStepRepository`** — structural subset of `StandardRepo` satisfied by any
|
|
30
|
+
kit repository that implements `purgeByField` + `countDocuments`.
|
|
31
|
+
- **`SCOPE_REQUIRED`** / **`REPOSITORY_UNAVAILABLE`** — blocker-code prefix constants.
|
|
32
|
+
|
|
33
|
+
#### `./tenant` — `resolveTenantField`
|
|
34
|
+
|
|
35
|
+
- **`resolveTenantField(config?)`** — returns the single `tenantField` string a
|
|
36
|
+
resource layer needs for `defineResource({ tenantField })`, or `false` when the
|
|
37
|
+
config disables tenant scoping (`false` / `{ enabled: false }` / `{ strategy: 'none' }`).
|
|
38
|
+
Four spine modules hand-rolled equivalent logic independently, each re-deriving
|
|
39
|
+
`'organizationId'` as the default; one copy omitted the disable branch, making
|
|
40
|
+
that package silently un-configurable. This function is the single definition.
|
|
41
|
+
|
|
42
|
+
Both additions are purely additive — no breaking changes.
|
|
43
|
+
|
|
7
44
|
## [0.17.0] - 2026-07-25
|
|
8
45
|
|
|
9
46
|
### Added — `./cleanup`: framework-free cleanup provider step contract
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { TenantPurgeOptions, TenantPurgeStrategy } from "../repository/types.mjs";
|
|
2
|
+
import { CleanupStep, CleanupStepContext } from "./types.mjs";
|
|
3
|
+
//#region src/cleanup/define-purge-step.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The repository surface a purge step needs — a structural subset of
|
|
6
|
+
* `StandardRepo`, so any kit repository satisfies it without an adapter.
|
|
7
|
+
* Both members are optional so an engine that never wired the repository
|
|
8
|
+
* yields a BLOCKER rather than a crash.
|
|
9
|
+
*/
|
|
10
|
+
interface PurgeStepRepository {
|
|
11
|
+
purgeByField?(field: string, value: unknown, strategy: TenantPurgeStrategy, options?: TenantPurgeOptions): Promise<{
|
|
12
|
+
processed: number;
|
|
13
|
+
ok: boolean;
|
|
14
|
+
error?: {
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
}>;
|
|
18
|
+
countDocuments?(filter: Record<string, unknown>): Promise<number>;
|
|
19
|
+
}
|
|
20
|
+
interface PurgeStepSpec {
|
|
21
|
+
/** Stable machine id, unique within a recipe. */
|
|
22
|
+
id: string;
|
|
23
|
+
/** Business record class for the preview (e.g. `'CRM contacts'`). */
|
|
24
|
+
resource: string;
|
|
25
|
+
/**
|
|
26
|
+
* Run-parameter key carrying the scope value (e.g. `'subjectId'`,
|
|
27
|
+
* `'organizationId'`). Read from `ctx.parameters` at plan AND execute time,
|
|
28
|
+
* so a retry replays the value the operator actually confirmed.
|
|
29
|
+
*/
|
|
30
|
+
parameter: string;
|
|
31
|
+
/** Document field matched against the parameter's value. */
|
|
32
|
+
field: string;
|
|
33
|
+
/** What to do with matched rows. */
|
|
34
|
+
strategy: TenantPurgeStrategy;
|
|
35
|
+
/** What survives — surfaced in the preview. */
|
|
36
|
+
retained?: string;
|
|
37
|
+
/** Non-blocking operator warnings. */
|
|
38
|
+
warnings?: readonly string[];
|
|
39
|
+
/** Rows per chunk. Kit default when omitted. */
|
|
40
|
+
batchSize?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Domain blockers beyond the built-in scope/availability checks (e.g.
|
|
43
|
+
* `'OPEN_CHECKOUTS:3'`). A non-empty result is a HARD STOP.
|
|
44
|
+
*/
|
|
45
|
+
guard?: (scope: string, ctx: CleanupStepContext) => Promise<readonly string[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Filter proving absence, when it differs from the match filter — e.g. an
|
|
48
|
+
* anonymize keyed on `_id` leaves the row in place, so absence must be
|
|
49
|
+
* proven by querying the redacted identifier instead.
|
|
50
|
+
*/
|
|
51
|
+
verifyFilter?: (scope: string) => Record<string, unknown>;
|
|
52
|
+
/** Overrides the generated check name. */
|
|
53
|
+
verifyName?: string;
|
|
54
|
+
}
|
|
55
|
+
/** Prefix for the blocker raised when the run carries no scope value. */
|
|
56
|
+
declare const SCOPE_REQUIRED = "CLEANUP_SCOPE_REQUIRED";
|
|
57
|
+
/** Prefix for the blocker raised when the repository cannot be reached. */
|
|
58
|
+
declare const REPOSITORY_UNAVAILABLE = "CLEANUP_REPOSITORY_UNAVAILABLE";
|
|
59
|
+
/**
|
|
60
|
+
* Build a `CleanupStep` from a repository + declaration.
|
|
61
|
+
*
|
|
62
|
+
* Invariants this owns, so no caller can get them wrong:
|
|
63
|
+
* - **fail-closed scoping** — no scope value, or no repository, is a
|
|
64
|
+
* BLOCKER, never a silent no-op and never an unscoped purge (which would
|
|
65
|
+
* hit every tenant);
|
|
66
|
+
* - **cancellation** — checked before work starts and threaded to the kit so
|
|
67
|
+
* a cancel lands between committed chunks, never mid-write;
|
|
68
|
+
* - **honest failure** — a failing purge returns `ok: false` so the composer
|
|
69
|
+
* stops the recipe (retention §8), and a thrown error is reported, never
|
|
70
|
+
* swallowed;
|
|
71
|
+
* - **verification** — absence is re-queried after the run, because a
|
|
72
|
+
* processed count alone is never proof (§9).
|
|
73
|
+
*/
|
|
74
|
+
declare function definePurgeStep(repository: PurgeStepRepository | undefined, spec: PurgeStepSpec): CleanupStep;
|
|
75
|
+
//#endregion
|
|
76
|
+
export { PurgeStepRepository, PurgeStepSpec, REPOSITORY_UNAVAILABLE, SCOPE_REQUIRED, definePurgeStep };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//#region src/cleanup/define-purge-step.ts
|
|
2
|
+
/** Prefix for the blocker raised when the run carries no scope value. */
|
|
3
|
+
const SCOPE_REQUIRED = "CLEANUP_SCOPE_REQUIRED";
|
|
4
|
+
/** Prefix for the blocker raised when the repository cannot be reached. */
|
|
5
|
+
const REPOSITORY_UNAVAILABLE = "CLEANUP_REPOSITORY_UNAVAILABLE";
|
|
6
|
+
function scopeOf(ctx, parameter) {
|
|
7
|
+
const raw = ctx.parameters?.[parameter];
|
|
8
|
+
return typeof raw === "string" && raw.length > 0 ? raw : void 0;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build a `CleanupStep` from a repository + declaration.
|
|
12
|
+
*
|
|
13
|
+
* Invariants this owns, so no caller can get them wrong:
|
|
14
|
+
* - **fail-closed scoping** — no scope value, or no repository, is a
|
|
15
|
+
* BLOCKER, never a silent no-op and never an unscoped purge (which would
|
|
16
|
+
* hit every tenant);
|
|
17
|
+
* - **cancellation** — checked before work starts and threaded to the kit so
|
|
18
|
+
* a cancel lands between committed chunks, never mid-write;
|
|
19
|
+
* - **honest failure** — a failing purge returns `ok: false` so the composer
|
|
20
|
+
* stops the recipe (retention §8), and a thrown error is reported, never
|
|
21
|
+
* swallowed;
|
|
22
|
+
* - **verification** — absence is re-queried after the run, because a
|
|
23
|
+
* processed count alone is never proof (§9).
|
|
24
|
+
*/
|
|
25
|
+
function definePurgeStep(repository, spec) {
|
|
26
|
+
const destructive = spec.strategy.type !== "skip";
|
|
27
|
+
const verifyName = spec.verifyName ?? `${spec.id}.verified`;
|
|
28
|
+
const filterFor = (scope) => spec.verifyFilter?.(scope) ?? { [spec.field]: scope };
|
|
29
|
+
return {
|
|
30
|
+
id: spec.id,
|
|
31
|
+
resource: spec.resource,
|
|
32
|
+
destructive,
|
|
33
|
+
async estimate(ctx) {
|
|
34
|
+
const scope = scopeOf(ctx, spec.parameter);
|
|
35
|
+
if (!scope) return {
|
|
36
|
+
resource: spec.resource,
|
|
37
|
+
estimated: 0,
|
|
38
|
+
blockers: [`${SCOPE_REQUIRED}:${spec.parameter}`]
|
|
39
|
+
};
|
|
40
|
+
if (!repository?.purgeByField) return {
|
|
41
|
+
resource: spec.resource,
|
|
42
|
+
estimated: 0,
|
|
43
|
+
blockers: [`${REPOSITORY_UNAVAILABLE}:${spec.id}`]
|
|
44
|
+
};
|
|
45
|
+
const estimated = await repository.countDocuments?.({ [spec.field]: scope }) ?? 0;
|
|
46
|
+
const blockers = await spec.guard?.(scope, ctx) ?? [];
|
|
47
|
+
return {
|
|
48
|
+
resource: spec.resource,
|
|
49
|
+
estimated,
|
|
50
|
+
...spec.retained === void 0 ? {} : { retained: spec.retained },
|
|
51
|
+
...blockers.length > 0 ? { blockers: [...blockers] } : {},
|
|
52
|
+
...spec.warnings === void 0 ? {} : { warnings: [...spec.warnings] }
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
async execute(ctx) {
|
|
56
|
+
await ctx.throwIfCancelled?.();
|
|
57
|
+
const scope = scopeOf(ctx, spec.parameter);
|
|
58
|
+
if (!scope || !repository?.purgeByField) return {
|
|
59
|
+
resource: spec.resource,
|
|
60
|
+
processed: 0,
|
|
61
|
+
ok: false,
|
|
62
|
+
error: scope ? `repository for '${spec.id}' is unavailable` : `missing run parameter '${spec.parameter}'`
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
const result = await repository.purgeByField(spec.field, scope, spec.strategy, {
|
|
66
|
+
...spec.batchSize === void 0 ? {} : { batchSize: spec.batchSize },
|
|
67
|
+
...ctx.signal ? { signal: ctx.signal } : {},
|
|
68
|
+
onProgress: async (event) => {
|
|
69
|
+
await ctx.onProgress?.({
|
|
70
|
+
resource: spec.resource,
|
|
71
|
+
processed: event.processed
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
resource: spec.resource,
|
|
77
|
+
processed: result.processed,
|
|
78
|
+
ok: result.ok,
|
|
79
|
+
...result.ok ? {} : { error: result.error?.message ?? "purge reported failure" }
|
|
80
|
+
};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return {
|
|
83
|
+
resource: spec.resource,
|
|
84
|
+
processed: 0,
|
|
85
|
+
ok: false,
|
|
86
|
+
error: error instanceof Error ? error.message : String(error)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
async verify(ctx) {
|
|
91
|
+
const scope = scopeOf(ctx, spec.parameter);
|
|
92
|
+
if (!scope || !repository?.countDocuments) return [{
|
|
93
|
+
name: verifyName,
|
|
94
|
+
ok: false,
|
|
95
|
+
detail: "could not verify — no scope or no count support"
|
|
96
|
+
}];
|
|
97
|
+
const remaining = await repository.countDocuments(filterFor(scope));
|
|
98
|
+
return [{
|
|
99
|
+
name: verifyName,
|
|
100
|
+
ok: remaining === 0,
|
|
101
|
+
detail: remaining === 0 ? `no ${spec.resource} still match the scope` : `${remaining} row(s) still match — cleanup incomplete`
|
|
102
|
+
}];
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
export { REPOSITORY_UNAVAILABLE, SCOPE_REQUIRED, definePurgeStep };
|
package/dist/cleanup/index.d.mts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { CleanupStep, CleanupStepCheck, CleanupStepContext, CleanupStepEstimate, CleanupStepExecuteContext, CleanupStepLogger, CleanupStepOutcome, CleanupStepProgress } from "./types.mjs";
|
|
2
|
-
|
|
2
|
+
import { PurgeStepRepository, PurgeStepSpec, REPOSITORY_UNAVAILABLE, SCOPE_REQUIRED, definePurgeStep } from "./define-purge-step.mjs";
|
|
3
|
+
export { type CleanupStep, type CleanupStepCheck, type CleanupStepContext, type CleanupStepEstimate, type CleanupStepExecuteContext, type CleanupStepLogger, type CleanupStepOutcome, type CleanupStepProgress, type PurgeStepRepository, type PurgeStepSpec, REPOSITORY_UNAVAILABLE, SCOPE_REQUIRED, definePurgeStep };
|
package/dist/cleanup/index.mjs
CHANGED
package/dist/tenant/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { ResolvedTenantConfig, TenantConfig, TenantFieldType, TenantStrategy } from "./types.mjs";
|
|
2
|
-
import { DEFAULT_TENANT_CONFIG, resolveTenantConfig } from "./resolve.mjs";
|
|
3
|
-
export { DEFAULT_TENANT_CONFIG, type ResolvedTenantConfig, type TenantConfig, type TenantFieldType, type TenantStrategy, resolveTenantConfig };
|
|
2
|
+
import { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField } from "./resolve.mjs";
|
|
3
|
+
export { DEFAULT_TENANT_CONFIG, type ResolvedTenantConfig, type TenantConfig, type TenantFieldType, type TenantStrategy, resolveTenantConfig, resolveTenantField };
|
package/dist/tenant/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { DEFAULT_TENANT_CONFIG, resolveTenantConfig } from "./resolve.mjs";
|
|
2
|
-
export { DEFAULT_TENANT_CONFIG, resolveTenantConfig };
|
|
1
|
+
import { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField } from "./resolve.mjs";
|
|
2
|
+
export { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField };
|
|
@@ -12,5 +12,21 @@ import { ResolvedTenantConfig, TenantConfig } from "./types.mjs";
|
|
|
12
12
|
type TenantDefaults = { [K in 'strategy' | 'enabled' | 'tenantField' | 'fieldType' | 'ref' | 'contextKey' | 'required']-?: Exclude<TenantConfig[K], undefined>; };
|
|
13
13
|
declare const DEFAULT_TENANT_CONFIG: TenantDefaults;
|
|
14
14
|
declare function resolveTenantConfig(config?: TenantConfig | boolean): ResolvedTenantConfig;
|
|
15
|
+
/**
|
|
16
|
+
* The single `tenantField` value a resource layer wants, or `false` when the
|
|
17
|
+
* option disables scoping entirely.
|
|
18
|
+
*
|
|
19
|
+
* Packages composing an arc resource need exactly this shape for
|
|
20
|
+
* `defineResource({ tenantField })`, and four spine modules independently
|
|
21
|
+
* hand-rolled it — each re-deriving `'organizationId'` as the default and
|
|
22
|
+
* unwrapping `{ tenantField }` by hand. One of those copies omitted the
|
|
23
|
+
* disable branch, so that package silently could not be configured
|
|
24
|
+
* company-wide. This wraps {@link resolveTenantConfig} so the default, the
|
|
25
|
+
* disable semantics, and the object-unwrapping have ONE definition.
|
|
26
|
+
*
|
|
27
|
+
* `false` / `{ enabled: false }` / `{ strategy: 'none' }` all mean "no tenant
|
|
28
|
+
* scoping" and all return `false` — callers get one thing to branch on.
|
|
29
|
+
*/
|
|
30
|
+
declare function resolveTenantField(config?: TenantConfig | boolean): string | false;
|
|
15
31
|
//#endregion
|
|
16
|
-
export { DEFAULT_TENANT_CONFIG, resolveTenantConfig };
|
|
32
|
+
export { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField };
|
package/dist/tenant/resolve.mjs
CHANGED
|
@@ -51,5 +51,25 @@ function resolveTenantConfig(config) {
|
|
|
51
51
|
enabled: cleaned.enabled ?? true
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The single `tenantField` value a resource layer wants, or `false` when the
|
|
56
|
+
* option disables scoping entirely.
|
|
57
|
+
*
|
|
58
|
+
* Packages composing an arc resource need exactly this shape for
|
|
59
|
+
* `defineResource({ tenantField })`, and four spine modules independently
|
|
60
|
+
* hand-rolled it — each re-deriving `'organizationId'` as the default and
|
|
61
|
+
* unwrapping `{ tenantField }` by hand. One of those copies omitted the
|
|
62
|
+
* disable branch, so that package silently could not be configured
|
|
63
|
+
* company-wide. This wraps {@link resolveTenantConfig} so the default, the
|
|
64
|
+
* disable semantics, and the object-unwrapping have ONE definition.
|
|
65
|
+
*
|
|
66
|
+
* `false` / `{ enabled: false }` / `{ strategy: 'none' }` all mean "no tenant
|
|
67
|
+
* scoping" and all return `false` — callers get one thing to branch on.
|
|
68
|
+
*/
|
|
69
|
+
function resolveTenantField(config) {
|
|
70
|
+
const resolved = resolveTenantConfig(config);
|
|
71
|
+
if (!resolved.enabled || resolved.strategy === "none") return false;
|
|
72
|
+
return resolved.tenantField;
|
|
73
|
+
}
|
|
54
74
|
//#endregion
|
|
55
|
-
export { DEFAULT_TENANT_CONFIG, resolveTenantConfig };
|
|
75
|
+
export { DEFAULT_TENANT_CONFIG, resolveTenantConfig, resolveTenantField };
|
package/dist/testing/index.d.mts
CHANGED
|
@@ -2,6 +2,6 @@ import { AggregateOpsSupport } from "../repository/capabilities.mjs";
|
|
|
2
2
|
import { ConformanceContext, ConformanceDoc, ConformanceFeatures, ConformanceHarness } from "./types.mjs";
|
|
3
3
|
import { runStandardRepoConformance } from "./conformance.mjs";
|
|
4
4
|
import { LockConformanceHarness, runLockAdapterConformance } from "./lock-conformance.mjs";
|
|
5
|
-
import { UsageConformanceHarness, runUsageStoreContract } from "./usage-conformance.mjs";
|
|
6
5
|
import { PurgeConformanceContext, PurgeConformanceHarness, runPurgeConformance } from "./purge-conformance.mjs";
|
|
6
|
+
import { UsageConformanceHarness, runUsageStoreContract } from "./usage-conformance.mjs";
|
|
7
7
|
export { type AggregateOpsSupport, type ConformanceContext, type ConformanceDoc, type ConformanceFeatures, type ConformanceHarness, type LockConformanceHarness, type PurgeConformanceContext, type PurgeConformanceHarness, type UsageConformanceHarness, runLockAdapterConformance, runPurgeConformance, runStandardRepoConformance, runUsageStoreContract };
|
package/dist/testing/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { runStandardRepoConformance } from "./conformance.mjs";
|
|
2
2
|
import { runLockAdapterConformance } from "./lock-conformance.mjs";
|
|
3
|
-
import { runUsageStoreContract } from "./usage-conformance.mjs";
|
|
4
3
|
import { runPurgeConformance } from "./purge-conformance.mjs";
|
|
4
|
+
import { runUsageStoreContract } from "./usage-conformance.mjs";
|
|
5
5
|
export { runLockAdapterConformance, runPurgeConformance, runStandardRepoConformance, runUsageStoreContract };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/repo-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Driver-agnostic repository primitives: hooks, Filter IR, operations, pagination, cache contract. Foundation for mongokit, sqlitekit, pgkit, and prismakit. Lean by design — no plugins ship here; each kit owns its own.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|