@cosmicdrift/kumiko-framework 0.305.0 → 0.306.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 (60) hide show
  1. package/package.json +4 -4
  2. package/src/api/__tests__/server-boot-guards.test.ts +1 -0
  3. package/src/api/__tests__/server-error-logging.test.ts +71 -0
  4. package/src/api/request-context.ts +5 -4
  5. package/src/api/routes.ts +26 -1
  6. package/src/bun-db/__tests__/closed-connection-retry.integration.test.ts +159 -0
  7. package/src/bun-db/__tests__/select-many-retry.integration.test.ts +138 -0
  8. package/src/bun-db/query.ts +42 -18
  9. package/src/changes.json +66 -0
  10. package/src/db/__tests__/pg-error.test.ts +14 -0
  11. package/src/db/__tests__/system-db-view-export.test.ts +107 -0
  12. package/src/db/__tests__/tenant-db-no-raw.test.ts +10 -0
  13. package/src/db/__tests__/with-systemdb-unsafe-raw-grant.test.ts +71 -0
  14. package/src/db/index.ts +1 -1
  15. package/src/db/pg-error.ts +13 -0
  16. package/src/db/queries/__tests__/{unsafe-read-retrying.test.ts → unsafe-read-retrying.integration.test.ts} +28 -28
  17. package/src/db/tenant-db.ts +90 -13
  18. package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +6 -1
  19. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +2 -2
  20. package/src/engine/__tests__/boot-validator.test.ts +1 -1
  21. package/src/engine/__tests__/tier-resolver-extension.test.ts +1 -1
  22. package/src/engine/extension-names.ts +55 -25
  23. package/src/engine/extensions/storage-provider.ts +14 -41
  24. package/src/engine/extensions/tenant-data.ts +4 -0
  25. package/src/engine/extensions/tenant-resource.ts +40 -0
  26. package/src/engine/extensions/user-data.ts +8 -7
  27. package/src/engine/feature-ast/__tests__/handler-header-roundtrip.test.ts +669 -0
  28. package/src/engine/feature-ast/__tests__/parse.test.ts +5 -3
  29. package/src/engine/feature-ast/__tests__/patch-update.test.ts +718 -0
  30. package/src/engine/feature-ast/__tests__/pattern-change-schema.test.ts +819 -0
  31. package/src/engine/feature-ast/entity-field-types.ts +41 -0
  32. package/src/engine/feature-ast/extractors/handlers.ts +217 -84
  33. package/src/engine/feature-ast/extractors/hooks.ts +72 -15
  34. package/src/engine/feature-ast/extractors/round2.ts +21 -0
  35. package/src/engine/feature-ast/extractors/shared.ts +9 -0
  36. package/src/engine/feature-ast/index.ts +11 -1
  37. package/src/engine/feature-ast/patch.ts +338 -5
  38. package/src/engine/feature-ast/patcher.ts +2 -2
  39. package/src/engine/feature-ast/pattern-change-schema.ts +1411 -0
  40. package/src/engine/feature-ast/patterns.ts +22 -15
  41. package/src/engine/feature-ast/render.ts +1 -0
  42. package/src/engine/feature-ui-extensions.ts +8 -7
  43. package/src/engine/index.ts +21 -5
  44. package/src/engine/types/extension-options-map.ts +1 -0
  45. package/src/engine/types/index.ts +6 -0
  46. package/src/jobs/__tests__/job-retention.integration.test.ts +316 -0
  47. package/src/jobs/__tests__/job-retry-enqueue-paths.integration.test.ts +309 -0
  48. package/src/jobs/__tests__/jobs.integration.test.ts +3 -3
  49. package/src/jobs/job-runner.ts +151 -14
  50. package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +113 -26
  51. package/src/pipeline/__tests__/dispatcher.test.ts +5 -0
  52. package/src/pipeline/__tests__/hook-systemdb-escape-hatch.integration.test.ts +246 -0
  53. package/src/pipeline/__tests__/idempotency-transient-failure.integration.test.ts +198 -0
  54. package/src/pipeline/__tests__/redis-pipeline.integration.test.ts +59 -0
  55. package/src/pipeline/dispatch-batch.ts +56 -13
  56. package/src/pipeline/idempotency.ts +16 -0
  57. package/src/pipeline/system-identity-switch.ts +22 -4
  58. package/src/testing/closed-connection-error.ts +62 -0
  59. package/src/testing/index.ts +1 -0
  60. package/src/bun-db/__tests__/select-many-retry.test.ts +0 -79
@@ -1,3 +1,12 @@
1
+ import type { DerivativeRendererPlugin } from "@cosmicdrift/kumiko-types/derivatives-types";
2
+ import type { OverlayResolverPlugin } from "../derivatives/derivatives-context";
3
+ import type { FileProviderPlugin } from "../files/provider-resolver";
4
+ import type { PrincipalStatusPlugin, TenantLifecycleStatusPlugin } from "./active-membership";
5
+ import type { TenantDataExtensionHooks } from "./extensions/tenant-data";
6
+ import type { TenantResourceExtensionHooks } from "./extensions/tenant-resource";
7
+ import type { UserDataExtensionOptions } from "./extensions/user-data";
8
+ import { TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin } from "./tier-resolver-extension";
9
+
1
10
  // Standardisierte Extension-Namen fuer Datenschutz-Hook-Achsen.
2
11
  //
3
12
  // Features registrieren Extensions via:
@@ -55,14 +64,12 @@ export const EXT_USER_DATA_ORDER = {
55
64
  export const EXT_TENANT_DATA = "tenantData" as const;
56
65
 
57
66
  /**
58
- * `storageProvider` — File-Storage-Plugin-Hooks (Crypto-Shredding fuer Files).
67
+ * `storageProvider` — file-storage-plugin tenant-destroy hook.
59
68
  *
60
- * Erwartete Hook-Methoden:
61
- * - `destroyTenant(tenantId, ctx) => Promise<void>`
62
- * - `destroySubject(subject, ctx) => Promise<{ deleted: number }>`
69
+ * Expected hook method: `destroyTenant(tenantId, ctx) => Promise<void>`.
63
70
  *
64
- * Registriert von: `storage-encryption` (Sprint 4).
65
- * Genutzt von: pluggable Provider (Local, MinIO, S3, R2).
71
+ * Registered by: `files-tenant-data`.
72
+ * Consumed by: pluggable providers (Local, MinIO, S3, R2).
66
73
  */
67
74
  export const EXT_STORAGE_PROVIDER = "storageProvider" as const;
68
75
 
@@ -148,37 +155,28 @@ export const EXT_DERIVATIVE_PUBLIC_PREDICATE = "derivativePublicPredicate" as co
148
155
  export const EXT_DERIVATIVE_OVERLAY_RESOLVER = "derivativeOverlayResolver" as const;
149
156
 
150
157
  /**
151
- * `searchAdapter` — Search-Adapter-Forget-Hooks (Meilisearch-Index-Cleanup
152
- * bei User-Forget oder Tenant-Destroy).
158
+ * `searchAdapter` — search-index tenant-destroy hook (Meilisearch index
159
+ * cleanup on tenant-destroy).
153
160
  *
154
- * Erwartete Hook-Methoden:
155
- * - `destroyTenant(tenantId, ctx) => Promise<void>`
156
- * - `eraseSubject(subject, ctx) => Promise<void>`
161
+ * Expected hook method: `destroyTenant(tenantId, ctx) => Promise<void>`.
157
162
  *
158
- * Registriert von: `tenant-lifecycle` (Sprint 5).
159
- * Genutzt von: Meilisearch- und andere Search-Adapter-Implementierungen.
163
+ * Consumed by: Meilisearch and other search-adapter implementations.
160
164
  */
161
165
  export const EXT_SEARCH_ADAPTER = "searchAdapter" as const;
162
166
 
163
167
  /**
164
- * `externalResource` — External-Service-Tenant-Cleanup
165
- * (Webhook-Subscriptions, Brevo-Empfaenger-Listen, Stripe-Customer-Account).
168
+ * `externalResource` — external-service tenant-destroy hook (webhook
169
+ * subscriptions, Brevo recipient lists, provider customer accounts).
166
170
  *
167
- * Erwartete Hook-Methoden:
168
- * - `destroyTenant(tenantId, ctx) => Promise<void>`
169
- *
170
- * Registriert von: `tenant-lifecycle` (Sprint 5).
171
+ * Expected hook method: `destroyTenant(tenantId, ctx) => Promise<void>`.
171
172
  */
172
173
  export const EXT_EXTERNAL_RESOURCE = "externalResource" as const;
173
174
 
174
175
  /**
175
- * `infraResource` — Pulumi-managed Resources pro Tenant
176
- * (Custom-Domain, Cert-Manager-Issuer, dedicated Pod/Volume).
176
+ * `infraResource` — Pulumi-managed per-tenant resource tenant-destroy hook
177
+ * (custom domain, cert-manager issuer, dedicated pod/volume).
177
178
  *
178
- * Erwartete Hook-Methoden:
179
- * - `destroyTenant(tenantId, ctx) => Promise<void>`
180
- *
181
- * Registriert von: `tenant-lifecycle` (Sprint 5).
179
+ * Expected hook method: `destroyTenant(tenantId, ctx) => Promise<void>`.
182
180
  */
183
181
  export const EXT_INFRA_RESOURCE = "infraResource" as const;
184
182
 
@@ -206,7 +204,39 @@ export const TENANT_MEMBERSHIPS_QUERY = "tenant:query:memberships" as const;
206
204
  export type KumikoExtensionName =
207
205
  | typeof EXT_USER_DATA
208
206
  | typeof EXT_TENANT_DATA
207
+ | typeof EXT_STORAGE_PROVIDER
208
+ | typeof EXT_SEARCH_ADAPTER
209
+ | typeof EXT_EXTERNAL_RESOURCE
210
+ | typeof EXT_INFRA_RESOURCE
211
+ | typeof EXT_FILE_PROVIDER
212
+ | typeof EXT_DERIVATIVE_RENDERER
213
+ | typeof EXT_DERIVATIVE_PUBLIC_PREDICATE
214
+ | typeof EXT_DERIVATIVE_OVERLAY_RESOLVER
215
+ | typeof EXT_PRINCIPAL_STATUS
216
+ | typeof EXT_TENANT_LIFECYCLE_STATUS;
217
+
218
+ /** The four `destroyTenant(tenantId, ctx)`-only resource-cleanup extension points. */
219
+ export type TenantResourceExtensionName =
209
220
  | typeof EXT_STORAGE_PROVIDER
210
221
  | typeof EXT_SEARCH_ADAPTER
211
222
  | typeof EXT_EXTERNAL_RESOURCE
212
223
  | typeof EXT_INFRA_RESOURCE;
224
+
225
+ // r.useExtension options-shape for framework-owned extension points; app/bundled-features-owned
226
+ // points augment co-located with their owner, since the framework never imports upward.
227
+ declare module "@cosmicdrift/kumiko-framework/engine" {
228
+ interface KumikoExtensionOptionsMap {
229
+ [EXT_TENANT_DATA]: TenantDataExtensionHooks;
230
+ [EXT_USER_DATA]: UserDataExtensionOptions;
231
+ [EXT_FILE_PROVIDER]: FileProviderPlugin;
232
+ [EXT_DERIVATIVE_RENDERER]: DerivativeRendererPlugin;
233
+ [EXT_DERIVATIVE_OVERLAY_RESOLVER]: OverlayResolverPlugin;
234
+ [EXT_PRINCIPAL_STATUS]: PrincipalStatusPlugin;
235
+ [EXT_TENANT_LIFECYCLE_STATUS]: TenantLifecycleStatusPlugin;
236
+ [EXT_STORAGE_PROVIDER]: TenantResourceExtensionHooks;
237
+ [EXT_SEARCH_ADAPTER]: TenantResourceExtensionHooks;
238
+ [EXT_EXTERNAL_RESOURCE]: TenantResourceExtensionHooks;
239
+ [EXT_INFRA_RESOURCE]: TenantResourceExtensionHooks;
240
+ [TENANT_TIER_RESOLVER_EXT]: TierResolverPlugin;
241
+ }
242
+ }
@@ -1,43 +1,16 @@
1
- // Hook signature types for EXT_STORAGE_PROVIDER (tenant-destroy binary cleanup).
2
- //
3
- // Mirror of tenant-data.ts, but the destroyTenant hook takes (tenantId, ctx)
4
- // rather than just (ctx) — runExtensionDestroyHooks (tenant-lifecycle/stages.ts)
5
- // passes tenantId as its own positional arg for every EXT_*_RESOURCE-style
6
- // extension point, not just this one. ctx here guarantees tenantId, db, plus
7
- // the optional fileProviderResolver/log; the richer stage-runner ctx
8
- // (tenant-lifecycle's DestructionStageCtx) is a structural superset, so a hook
9
- // typed against this minimal ctx is safely assignable wherever that richer ctx
10
- // is passed.
11
- //
12
- // `db` is a raw DbRunner, NOT a tenant-scoped TenantDb — unlike
13
- // TenantDataHookCtx.db (EXT_TENANT_DATA), runExtensionDestroyHooks hands every
14
- // EXT_*_RESOURCE-style hook (this one included) the stage runner's own
15
- // DestructionStageCtx.db straight through, with no tenant filter and no
16
- // escapeHatch/unsafeRaw gate — there is nothing to declare, because nothing
17
- // ever wraps it. That's deliberate for this stage family: wiping a whole
18
- // search index, an S3 prefix, or infra resources is inherently a cross-tenant,
19
- // bulk operation, not a row-scoped one. A hook that needs a plain read across
20
- // tenants (kumiko-framework#3035: does a foreign tenant's file_refs row still
1
+ // EXT_STORAGE_PROVIDER (tenant-destroy binary cleanup) is one of the four
2
+ // EXT_*_RESOURCE-style extension points — see tenant-resource.ts for the
3
+ // shared hook-signature contract. Kept as aliases so the existing public
4
+ // exports/imports stay valid; a hook that needs a plain cross-tenant read
5
+ // (kumiko-framework#3035: does a foreign tenant's file_refs row still
21
6
  // reference a key under THIS tenant's storage prefix) uses `db` directly via
22
- // executeRawQueryRead — there is no `ctx.systemDb` here (that belongs to
23
- // HandlerContext / r.systemScope() write-handler dispatch, a different code
24
- // path this stage runner never goes through).
25
- import type { FileProviderResolver } from "@cosmicdrift/kumiko-types/file-provider-resolver-types";
26
- import type { DbRunner } from "../../db/connection";
27
- import type { TenantId } from "../types";
7
+ // executeRawQueryRead — there is no `ctx.systemDb` here.
8
+ import type {
9
+ TenantResourceDestroyHook,
10
+ TenantResourceExtensionHooks,
11
+ TenantResourceHookCtx,
12
+ } from "./tenant-resource";
28
13
 
29
- export interface StorageProviderHookCtx {
30
- readonly tenantId: TenantId;
31
- readonly db: DbRunner;
32
- readonly fileProviderResolver?: FileProviderResolver;
33
- readonly log?: (message: string) => void;
34
- }
35
-
36
- export type StorageProviderDestroyTenantHook = (
37
- tenantId: TenantId,
38
- ctx: StorageProviderHookCtx,
39
- ) => Promise<void>;
40
-
41
- export interface StorageProviderExtensionHooks {
42
- readonly destroyTenant: StorageProviderDestroyTenantHook;
43
- }
14
+ export type StorageProviderHookCtx = TenantResourceHookCtx;
15
+ export type StorageProviderDestroyTenantHook = TenantResourceDestroyHook;
16
+ export type StorageProviderExtensionHooks = TenantResourceExtensionHooks;
@@ -28,3 +28,7 @@ export type TenantDataDestroyHook = (ctx: TenantDataHookCtx) => Promise<void>;
28
28
  export interface TenantDataExtensionHooks {
29
29
  readonly destroy: TenantDataDestroyHook;
30
30
  }
31
+
32
+ export function isTenantDataExtensionHooks(o: unknown): o is TenantDataExtensionHooks {
33
+ return typeof o === "object" && o !== null && "destroy" in o && typeof o.destroy === "function";
34
+ }
@@ -0,0 +1,40 @@
1
+ // Hook signature types shared by the four EXT_*_RESOURCE-style extension
2
+ // points (storageProvider, searchAdapter, externalResource, infraResource).
3
+ // runExtensionDestroyHooks (tenant-lifecycle/stages.ts) passes tenantId as
4
+ // its own positional arg for all of them, not just storageProvider — the
5
+ // stage runner's DestructionStageCtx is a structural superset of the ctx
6
+ // below, so a hook typed against it is safely assignable wherever the
7
+ // richer ctx is passed.
8
+ //
9
+ // `db` is a raw DbRunner, not a tenant-scoped TenantDb: wiping a whole
10
+ // search index, an S3 prefix, or infra resources is inherently a
11
+ // cross-tenant, bulk operation, not a row-scoped one — there is no
12
+ // escapeHatch/unsafeRaw gate here because nothing ever wraps `db`.
13
+ import type { FileProviderResolver } from "@cosmicdrift/kumiko-types/file-provider-resolver-types";
14
+ import type { DbRunner } from "../../db/connection";
15
+ import type { TenantId } from "../types";
16
+
17
+ export interface TenantResourceHookCtx {
18
+ readonly tenantId: TenantId;
19
+ readonly db: DbRunner;
20
+ readonly fileProviderResolver?: FileProviderResolver;
21
+ readonly log?: (message: string) => void;
22
+ }
23
+
24
+ export type TenantResourceDestroyHook = (
25
+ tenantId: TenantId,
26
+ ctx: TenantResourceHookCtx,
27
+ ) => Promise<void>;
28
+
29
+ export interface TenantResourceExtensionHooks {
30
+ readonly destroyTenant: TenantResourceDestroyHook;
31
+ }
32
+
33
+ export function isTenantResourceExtensionHooks(o: unknown): o is TenantResourceExtensionHooks {
34
+ return (
35
+ typeof o === "object" &&
36
+ o !== null &&
37
+ "destroyTenant" in o &&
38
+ typeof o.destroyTenant === "function"
39
+ );
40
+ }
@@ -166,15 +166,16 @@ export type UserDataDeleteHook = (
166
166
  ) => Promise<UserDataDeleteHookResult>;
167
167
 
168
168
  /**
169
- * Komplette Hook-Tafel für EXT_USER_DATA. Sprint 2 user-data-rights
170
- * deklariert das via `r.extendsRegistrar(EXT_USER_DATA, { hooks: ... })`,
171
- * konsumierende Features liefern beide Hooks via
172
- * `r.useExtension(EXT_USER_DATA, "<entity>", { export, delete })`.
173
- *
174
- * Kein Hook ist optional — beide MÜSSEN registriert sein. Boot-Check
175
- * (Sprint 2) prüft das.
169
+ * Full hook table for EXT_USER_DATA. Export-only is a legitimate partial
170
+ * contract, so registrations are typed by `UserDataExtensionOptions`.
176
171
  */
177
172
  export interface UserDataExtensionHooks {
178
173
  readonly export: UserDataExportHook;
179
174
  readonly delete: UserDataDeleteHook;
180
175
  }
176
+
177
+ // At least one hook is required: an empty bag would silently register neither export nor forget.
178
+ export type UserDataExtensionOptions = (
179
+ | { readonly export: UserDataExportHook; readonly delete?: UserDataDeleteHook }
180
+ | { readonly export?: UserDataExportHook; readonly delete: UserDataDeleteHook }
181
+ ) & { readonly order?: number };