@classytic/repo-core 0.6.0 → 0.6.1

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 CHANGED
@@ -4,6 +4,24 @@ 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.6.1] - 2026-07-04
8
+
9
+ ### Changed — `TenantConfig` optionals widened to `T | undefined` (P10)
10
+
11
+ Every optional prop on `TenantConfig` (`strategy`, `enabled`, `tenantField`,
12
+ `fieldType`, `ref`, `contextKey`, `required`, `resolve`) is now typed
13
+ `T | undefined`, so downstream packages compiling with
14
+ `exactOptionalPropertyTypes: true` can extend it without redeclaring props
15
+ (unblocks `@classytic/ledger`'s `MultiTenantConfig.required` exception).
16
+
17
+ `resolveTenantConfig` strips explicit-`undefined` keys before spreading
18
+ over `DEFAULT_TENANT_CONFIG`, so `{ required: maybeUndefined }` can never
19
+ clobber a default with `undefined` — previously the un-widened type made
20
+ that unrepresentable; now it's handled at runtime and pinned by tests.
21
+ `DEFAULT_TENANT_CONFIG`'s annotation moved from `Required<Pick<...>>` to an
22
+ `Exclude<..., undefined>` mapping (`-?` does not strip an explicit
23
+ undefined union member). No runtime behavior change for existing callers.
24
+
7
25
  ## [0.6.0] - 2026-06-11
8
26
 
9
27
  Standardization release. Coordinated with mongokit 3.16 + sqlitekit 0.6.
@@ -10,18 +10,8 @@ import { ResolvedTenantConfig, TenantConfig } from "./types.mjs";
10
10
  * runtime default — `Pick<TenantConfig, 'fieldType'>` extension preserves
11
11
  * type-level alignment without forcing a runtime default change.
12
12
  */
13
- declare const DEFAULT_TENANT_CONFIG: Required<Pick<TenantConfig, 'strategy' | 'enabled' | 'tenantField' | 'fieldType' | 'ref' | 'contextKey' | 'required'>>;
14
- /**
15
- * Resolve a possibly-partial {@link TenantConfig} against the defaults.
16
- *
17
- * - `false` → `enabled: false`, `strategy: 'none'`, `required: false`.
18
- * - `true` / `undefined` → default field strategy.
19
- * - Object with `strategy: 'custom'` → `resolve` is required; throws
20
- * otherwise so the misconfiguration surfaces at boot, not runtime.
21
- * - Object with `strategy: 'none'` → `enabled: false` (preserves
22
- * user-supplied `tenantField` / `fieldType` / `ref` so the doc field
23
- * stays correctly typed even with scoping off).
24
- */
13
+ type TenantDefaults = { [K in 'strategy' | 'enabled' | 'tenantField' | 'fieldType' | 'ref' | 'contextKey' | 'required']-?: Exclude<TenantConfig[K], undefined> };
14
+ declare const DEFAULT_TENANT_CONFIG: TenantDefaults;
25
15
  declare function resolveTenantConfig(config?: TenantConfig | boolean): ResolvedTenantConfig;
26
16
  //#endregion
27
17
  export { DEFAULT_TENANT_CONFIG, resolveTenantConfig };
@@ -1,13 +1,4 @@
1
1
  //#region src/tenant/resolve.ts
2
- /**
3
- * Sensible defaults for a freshly-built package (field strategy).
4
- *
5
- * `fieldType: 'objectId'` is the recommended default for new Mongo-shaped
6
- * kits because it enables `$lookup` / `.populate()`. Existing kits that
7
- * historically defaulted to `'string'` (mongokit pre-3.x) keep their own
8
- * runtime default — `Pick<TenantConfig, 'fieldType'>` extension preserves
9
- * type-level alignment without forcing a runtime default change.
10
- */
11
2
  const DEFAULT_TENANT_CONFIG = {
12
3
  strategy: "field",
13
4
  enabled: true,
@@ -17,17 +8,11 @@ const DEFAULT_TENANT_CONFIG = {
17
8
  contextKey: "organizationId",
18
9
  required: true
19
10
  };
20
- /**
21
- * Resolve a possibly-partial {@link TenantConfig} against the defaults.
22
- *
23
- * - `false` → `enabled: false`, `strategy: 'none'`, `required: false`.
24
- * - `true` / `undefined` → default field strategy.
25
- * - Object with `strategy: 'custom'` → `resolve` is required; throws
26
- * otherwise so the misconfiguration surfaces at boot, not runtime.
27
- * - Object with `strategy: 'none'` → `enabled: false` (preserves
28
- * user-supplied `tenantField` / `fieldType` / `ref` so the doc field
29
- * stays correctly typed even with scoping off).
30
- */
11
+ function stripUndefined(obj) {
12
+ const out = {};
13
+ for (const [k, v] of Object.entries(obj)) if (v !== void 0) out[k] = v;
14
+ return out;
15
+ }
31
16
  function resolveTenantConfig(config) {
32
17
  if (config === false) return {
33
18
  ...DEFAULT_TENANT_CONFIG,
@@ -36,33 +21,34 @@ function resolveTenantConfig(config) {
36
21
  required: false
37
22
  };
38
23
  if (config === true || config === void 0) return { ...DEFAULT_TENANT_CONFIG };
39
- const strategy = config.strategy ?? (config.enabled === false ? "none" : "field");
40
- const contextKey = config.contextKey ?? config.tenantField ?? DEFAULT_TENANT_CONFIG.contextKey;
24
+ const cleaned = stripUndefined(config);
25
+ const strategy = cleaned.strategy ?? (cleaned.enabled === false ? "none" : "field");
26
+ const contextKey = cleaned.contextKey ?? cleaned.tenantField ?? DEFAULT_TENANT_CONFIG.contextKey;
41
27
  if (strategy === "none") return {
42
28
  ...DEFAULT_TENANT_CONFIG,
43
- ...config,
29
+ ...cleaned,
44
30
  contextKey,
45
31
  strategy: "none",
46
32
  enabled: false,
47
33
  required: false
48
34
  };
49
35
  if (strategy === "custom") {
50
- if (typeof config.resolve !== "function") throw new Error("[repo-core] TenantConfig.strategy 'custom' requires a 'resolve' function");
36
+ if (typeof cleaned.resolve !== "function") throw new Error("[repo-core] TenantConfig.strategy 'custom' requires a 'resolve' function");
51
37
  return {
52
38
  ...DEFAULT_TENANT_CONFIG,
53
- ...config,
39
+ ...cleaned,
54
40
  contextKey,
55
41
  strategy: "custom",
56
- enabled: config.enabled ?? true,
57
- resolve: config.resolve
42
+ enabled: cleaned.enabled ?? true,
43
+ resolve: cleaned.resolve
58
44
  };
59
45
  }
60
46
  return {
61
47
  ...DEFAULT_TENANT_CONFIG,
62
- ...config,
48
+ ...cleaned,
63
49
  contextKey,
64
50
  strategy: "field",
65
- enabled: config.enabled ?? true
51
+ enabled: cleaned.enabled ?? true
66
52
  };
67
53
  }
68
54
  //#endregion
@@ -50,7 +50,7 @@ interface TenantConfig {
50
50
  *
51
51
  * @default 'field'
52
52
  */
53
- strategy?: TenantStrategy;
53
+ strategy?: TenantStrategy | undefined;
54
54
  /**
55
55
  * Whether tenant scoping is active. When `false`, the package runs in
56
56
  * single-tenant mode — no filter injection, no tenant field on documents.
@@ -58,27 +58,27 @@ interface TenantConfig {
58
58
  *
59
59
  * @default true
60
60
  */
61
- enabled?: boolean;
61
+ enabled?: boolean | undefined;
62
62
  /**
63
63
  * Document / column field name that stores the tenant id. Used when
64
64
  * `strategy === 'field'`.
65
65
  *
66
66
  * @default 'organizationId'
67
67
  */
68
- tenantField?: string;
68
+ tenantField?: string | undefined;
69
69
  /**
70
70
  * How to store / cast the tenant id.
71
71
  *
72
72
  * @default 'objectId'
73
73
  */
74
- fieldType?: TenantFieldType;
74
+ fieldType?: TenantFieldType | undefined;
75
75
  /**
76
76
  * Mongoose ref for `'objectId'` types. Ignored by SQL kits and when
77
77
  * `fieldType === 'string'`.
78
78
  *
79
79
  * @default 'organization'
80
80
  */
81
- ref?: string;
81
+ ref?: string | undefined;
82
82
  /**
83
83
  * Which key on the repository context to read the tenant id from.
84
84
  *
@@ -92,14 +92,14 @@ interface TenantConfig {
92
92
  *
93
93
  * @default tenantField ?? 'organizationId'
94
94
  */
95
- contextKey?: string;
95
+ contextKey?: string | undefined;
96
96
  /**
97
97
  * Whether the field is required. When `false`, the package permits
98
98
  * unscoped / cross-tenant reads (typically only for admin paths).
99
99
  *
100
100
  * @default true
101
101
  */
102
- required?: boolean;
102
+ required?: boolean | undefined;
103
103
  /**
104
104
  * Custom resolver — called when `strategy === 'custom'` to produce the
105
105
  * filter object injected into queries. Packages pass the request /
@@ -121,7 +121,7 @@ interface TenantConfig {
121
121
  * }
122
122
  * ```
123
123
  */
124
- resolve?: (ctx: Record<string, unknown>) => Record<string, unknown>;
124
+ resolve?: (ctx: Record<string, unknown>) => Record<string, unknown> | undefined;
125
125
  }
126
126
  /**
127
127
  * Resolved shape returned by `resolveTenantConfig`. Always includes the
@@ -136,7 +136,7 @@ type ResolvedTenantConfig = {
136
136
  ref: string;
137
137
  contextKey: string;
138
138
  required: boolean;
139
- resolve?: TenantConfig['resolve'];
139
+ resolve?: TenantConfig['resolve'] | undefined;
140
140
  };
141
141
  //#endregion
142
142
  export { ResolvedTenantConfig, TenantConfig, TenantFieldType, TenantStrategy };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/repo-core",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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,