@cosmicdrift/kumiko-framework 0.367.0 → 0.368.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 (233) hide show
  1. package/AGENTS.md +1 -7
  2. package/README.md +1 -1
  3. package/agents/examples/access-control.md +2 -2
  4. package/agents/examples/anonymous-access-multitenant.md +2 -2
  5. package/agents/examples/anonymous-access.md +3 -3
  6. package/agents/examples/apex-landing.md +1 -1
  7. package/agents/examples/apex-surface-auth.md +6 -7
  8. package/agents/examples/api-worker-split.md +7 -8
  9. package/agents/examples/auth-claims.md +2 -1
  10. package/agents/examples/auth-foundation-providers.md +3 -3
  11. package/agents/examples/auth-mfa.md +3 -4
  12. package/agents/examples/basic-entity.md +3 -4
  13. package/agents/examples/chat-channels.md +4 -4
  14. package/agents/examples/compliance-profiles-demo.md +2 -3
  15. package/agents/examples/cross-feature-events.md +4 -4
  16. package/agents/examples/crypto-shredding-hr.md +14 -9
  17. package/agents/examples/currencies-global.md +3 -4
  18. package/agents/examples/currencies-per-tenant.md +3 -4
  19. package/agents/examples/custom-fields-basic.md +2 -3
  20. package/agents/examples/custom-handlers.md +3 -5
  21. package/agents/examples/dashboard-stats.md +2 -3
  22. package/agents/examples/delivery-notifications.md +5 -6
  23. package/agents/examples/e2e-generator.md +1 -1
  24. package/agents/examples/embedded-entity-form.md +2 -3
  25. package/agents/examples/embedded.md +2 -3
  26. package/agents/examples/encrypted-tenant-config.md +5 -6
  27. package/agents/examples/error-contract.md +3 -4
  28. package/agents/examples/event-sourcing.md +19 -23
  29. package/agents/examples/feature-toggles.md +4 -4
  30. package/agents/examples/field-access.md +3 -4
  31. package/agents/examples/files-post-processing.md +12 -13
  32. package/agents/examples/golden-path.md +208 -0
  33. package/agents/examples/idempotency.md +2 -2
  34. package/agents/examples/inbound-mail-triage.md +7 -7
  35. package/agents/examples/lane-routing.md +3 -3
  36. package/agents/examples/ledger-banking.md +2 -3
  37. package/agents/examples/ledger-invoicing.md +2 -3
  38. package/agents/examples/legal-pages.md +1 -1
  39. package/agents/examples/lifecycle-hooks.md +3 -3
  40. package/agents/examples/managed-config.md +3 -4
  41. package/agents/examples/managed-pages.md +2 -2
  42. package/agents/examples/notes-history-basic.md +4 -5
  43. package/agents/examples/ownership.md +3 -5
  44. package/agents/examples/pdf-audit-export.md +2 -3
  45. package/agents/examples/pipeline-basics.md +2 -2
  46. package/agents/examples/public-share-token.md +3 -3
  47. package/agents/examples/rate-limiting.md +2 -1
  48. package/agents/examples/realtime-sse.md +2 -3
  49. package/agents/examples/record-detail-layout.md +1 -1
  50. package/agents/examples/reference-data.md +3 -3
  51. package/agents/examples/relations.md +2 -2
  52. package/agents/examples/screens-nav.md +1 -1
  53. package/agents/examples/search.md +2 -2
  54. package/agents/examples/secrets-demo.md +8 -8
  55. package/agents/examples/security-baseline.md +2 -2
  56. package/agents/examples/session-revocation.md +7 -8
  57. package/agents/examples/state-machine.md +181 -165
  58. package/agents/examples/tags-basic.md +2 -3
  59. package/agents/examples/tenant-isolation.md +2 -3
  60. package/agents/examples/tenant-settings.md +2 -2
  61. package/agents/examples/tier-admin.md +3 -4
  62. package/agents/examples/timezones.md +3 -3
  63. package/agents/examples/user-data-rights.md +12 -8
  64. package/agents/examples/user-profile.md +3 -3
  65. package/agents/examples/webhook-step.md +2 -3
  66. package/agents/examples/wizard-form.md +4 -5
  67. package/agents/examples/workflow-engine.md +7 -3
  68. package/agents/examples/writeform-section.md +1 -1
  69. package/agents/examples.md +1 -0
  70. package/dist/consumer-cli.js +2 -3
  71. package/dist/crypto/index.d.ts +1 -1
  72. package/dist/crypto/index.js +1 -1
  73. package/dist/crypto/owner-transition-rekey.js +2 -0
  74. package/dist/crypto/pg-kms-adapter.js +1 -1
  75. package/dist/crypto/subject-resolver.d.ts +2 -0
  76. package/dist/crypto/subject-resolver.js +30 -15
  77. package/dist/db/bun-db-deprecated.d.ts +10 -0
  78. package/dist/{bun-db/index.js → db/bun-db-deprecated.js} +4 -3
  79. package/dist/db/config-seed.js +1 -1
  80. package/dist/db/connection.d.ts +23 -5
  81. package/dist/db/connection.js +10 -5
  82. package/dist/db/dialect.js +1 -1
  83. package/dist/db/entity-field-encryption.d.ts +5 -0
  84. package/dist/db/entity-field-encryption.js +29 -0
  85. package/dist/db/entity-table-meta.js +6 -5
  86. package/dist/db/event-store-executor-read.js +26 -5
  87. package/dist/db/index.d.ts +4 -4
  88. package/dist/db/index.js +1 -1
  89. package/dist/db/migrate-runner.js +1 -1
  90. package/dist/db/queries/backfill-pii.js +16 -9
  91. package/dist/db/queries/ddl.js +1 -1
  92. package/dist/db/queries/event-store.d.ts +2 -0
  93. package/dist/db/queries/event-store.js +26 -0
  94. package/dist/db/queries/test-stack.js +1 -1
  95. package/dist/db/query.d.ts +105 -1
  96. package/dist/db/query.js +1450 -3
  97. package/dist/{stack/table-helpers.d.ts → db/table-push.d.ts} +5 -15
  98. package/dist/{stack/table-helpers.js → db/table-push.js} +6 -46
  99. package/dist/db/tenant-db.d.ts +11 -0
  100. package/dist/db/tenant-db.js +51 -12
  101. package/dist/derivatives/derivatives-context.d.ts +1 -1
  102. package/dist/derivatives/derivatives-context.js +1 -1
  103. package/dist/engine/boot-validator/gdpr-storage.js +3 -3
  104. package/dist/engine/boot-validator/pii-retention.js +3 -3
  105. package/dist/engine/boot-validator/record-owned.js +3 -3
  106. package/dist/engine/boot-validator/screens.js +8 -1
  107. package/dist/engine/bridge-stub.d.ts +4 -0
  108. package/dist/engine/bridge-stub.js +62 -0
  109. package/dist/engine/event-helpers.d.ts +2 -1
  110. package/dist/engine/event-helpers.js +1 -0
  111. package/dist/engine/feature-config-events-jobs.js +2 -2
  112. package/dist/engine/index.d.ts +1 -0
  113. package/dist/engine/index.js +1 -0
  114. package/dist/es-ops/operations-schema.js +1 -1
  115. package/dist/event-store/admin-api.js +1 -1
  116. package/dist/event-store/archive.js +1 -1
  117. package/dist/event-store/errors.d.ts +2 -1
  118. package/dist/event-store/errors.js +6 -2
  119. package/dist/event-store/event-store.js +17 -9
  120. package/dist/event-store/events-schema.js +1 -1
  121. package/dist/event-store/rebuild-dead-letter.js +3 -3
  122. package/dist/event-store/snapshot.js +1 -1
  123. package/dist/event-store/upcaster-dead-letter.js +3 -3
  124. package/dist/files/create-file-ref-from-bytes.js +1 -1
  125. package/dist/files/file-routes.js +1 -1
  126. package/dist/internal/stack-support.d.ts +12 -0
  127. package/dist/internal/stack-support.js +12 -0
  128. package/dist/internal/testing.d.ts +4 -0
  129. package/dist/internal/testing.js +5 -0
  130. package/dist/jobs/job-runner.js +23 -11
  131. package/dist/migrations/pending-rebuilds.js +1 -1
  132. package/dist/migrations/pii-event-backfill.js +2 -2
  133. package/dist/pipeline/dispatch-shared.js +43 -23
  134. package/dist/pipeline/dispatch-write.js +6 -2
  135. package/dist/pipeline/dispatcher-utils.js +6 -2
  136. package/dist/pipeline/event-consumer-state.js +1 -1
  137. package/dist/pipeline/projection-rebuild.d.ts +1 -0
  138. package/dist/pipeline/projection-rebuild.js +1 -1
  139. package/dist/pipeline/projection-state.js +1 -1
  140. package/dist/pipeline/system-hooks.js +1 -1
  141. package/dist/schema-cli.js +3 -3
  142. package/dist/search/purge-subject.js +20 -10
  143. package/dist/seeding/index.d.ts +1 -0
  144. package/dist/seeding/index.js +1 -0
  145. package/dist/stack/index.d.ts +10 -9
  146. package/dist/stack/index.js +11 -19
  147. package/dist/testing/e2e-generator.d.ts +5 -49
  148. package/dist/testing/e2e-generator.js +5 -382
  149. package/dist/testing/handler-context.d.ts +5 -4
  150. package/dist/testing/handler-context.js +5 -69
  151. package/dist/testing/index.d.ts +10 -26
  152. package/dist/testing/index.js +10 -34
  153. package/dist/testing/real-providers.d.ts +5 -3
  154. package/dist/testing/real-providers.js +5 -21
  155. package/package.json +37 -9
  156. package/src/changes.json +58 -0
  157. package/src/scripts/codemod/README.md +43 -0
  158. package/src/scripts/codemod/codemod-support.ts +276 -0
  159. package/src/scripts/codemod/db-factory.ts +810 -0
  160. package/src/scripts/codemod/feature-factory.ts +429 -0
  161. package/src/scripts/codemod/s3-imports.ts +274 -0
  162. package/src/scripts/codemod/tenant-db-options.ts +454 -0
  163. package/src/scripts/codemod/testing-move-targets.ts +161 -0
  164. package/src/scripts/codemod/testing-move.ts +520 -0
  165. package/dist/bun-db/connection.d.ts +0 -17
  166. package/dist/bun-db/connection.js +0 -40
  167. package/dist/bun-db/index.d.ts +0 -4
  168. package/dist/bun-db/query.d.ts +0 -106
  169. package/dist/bun-db/query.js +0 -1457
  170. package/dist/db/api.d.ts +0 -19
  171. package/dist/db/api.js +0 -23
  172. package/dist/db/bun-provider.d.ts +0 -2
  173. package/dist/db/bun-provider.js +0 -28
  174. package/dist/db/postgres-provider.d.ts +0 -2
  175. package/dist/db/postgres-provider.js +0 -21
  176. package/dist/db/query-api.d.ts +0 -2
  177. package/dist/db/query-api.js +0 -6
  178. package/dist/stack/db.d.ts +0 -22
  179. package/dist/stack/db.js +0 -67
  180. package/dist/stack/drain-event-consumers.d.ts +0 -4
  181. package/dist/stack/drain-event-consumers.js +0 -51
  182. package/dist/stack/drain-jobs.d.ts +0 -22
  183. package/dist/stack/drain-jobs.js +0 -141
  184. package/dist/stack/event-collector.d.ts +0 -9
  185. package/dist/stack/event-collector.js +0 -12
  186. package/dist/stack/push-entity-projection-tables.d.ts +0 -24
  187. package/dist/stack/push-entity-projection-tables.js +0 -74
  188. package/dist/stack/redis.d.ts +0 -17
  189. package/dist/stack/redis.js +0 -52
  190. package/dist/stack/request-helper.d.ts +0 -35
  191. package/dist/stack/request-helper.js +0 -144
  192. package/dist/stack/test-stack.d.ts +0 -192
  193. package/dist/stack/test-stack.js +0 -429
  194. package/dist/testing/access-assertions.d.ts +0 -2
  195. package/dist/testing/access-assertions.js +0 -13
  196. package/dist/testing/assertions.d.ts +0 -10
  197. package/dist/testing/assertions.js +0 -22
  198. package/dist/testing/boot-validator-fixture.d.ts +0 -3
  199. package/dist/testing/boot-validator-fixture.js +0 -101
  200. package/dist/testing/closed-connection-error.d.ts +0 -7
  201. package/dist/testing/closed-connection-error.js +0 -46
  202. package/dist/testing/db-cleanup.d.ts +0 -9
  203. package/dist/testing/db-cleanup.js +0 -31
  204. package/dist/testing/expect-error.d.ts +0 -2
  205. package/dist/testing/expect-error.js +0 -22
  206. package/dist/testing/file-provider-contract.d.ts +0 -2
  207. package/dist/testing/file-provider-contract.js +0 -102
  208. package/dist/testing/http-cookies.d.ts +0 -7
  209. package/dist/testing/http-cookies.js +0 -47
  210. package/dist/testing/late-bound.d.ts +0 -9
  211. package/dist/testing/late-bound.js +0 -31
  212. package/dist/testing/manual-clock.d.ts +0 -14
  213. package/dist/testing/manual-clock.js +0 -12
  214. package/dist/testing/multipart-helper.d.ts +0 -22
  215. package/dist/testing/multipart-helper.js +0 -77
  216. package/dist/testing/mutable-master-key-provider.d.ts +0 -7
  217. package/dist/testing/mutable-master-key-provider.js +0 -35
  218. package/dist/testing/observability-recorder.d.ts +0 -9
  219. package/dist/testing/observability-recorder.js +0 -29
  220. package/dist/testing/seed.d.ts +0 -9
  221. package/dist/testing/seed.js +0 -21
  222. package/dist/testing/shared-entities.d.ts +0 -286
  223. package/dist/testing/shared-entities.js +0 -56
  224. package/dist/testing/utils.d.ts +0 -1
  225. package/dist/testing/utils.js +0 -1
  226. package/dist/testing/wait-for.d.ts +0 -20
  227. package/dist/testing/wait-for.js +0 -47
  228. package/dist/testing/without-ambient-temporal.d.ts +0 -5
  229. package/dist/testing/without-ambient-temporal.js +0 -17
  230. package/dist/testing/zip-fixture.d.ts +0 -1
  231. package/dist/testing/zip-fixture.js +0 -33
  232. /package/dist/{stack/test-users.d.ts → seeding/seed-users.d.ts} +0 -0
  233. /package/dist/{stack/test-users.js → seeding/seed-users.js} +0 -0
@@ -91,15 +91,14 @@ import {
91
91
  createEnvMasterKeyProvider,
92
92
  type MasterKeyProvider,
93
93
  } from "@cosmicdrift/kumiko-framework/secrets";
94
+ import { createTestUser, testTenantId } from "@cosmicdrift/kumiko-framework/seeding";
94
95
  import {
95
- createTestUser,
96
96
  resetEventStore,
97
97
  setupTestStack,
98
98
  type TestStack,
99
- testTenantId,
100
99
  unsafeCreateEntityTable,
101
100
  unsafePushTables,
102
- } from "@cosmicdrift/kumiko-framework/stack";
101
+ } from "@cosmicdrift/kumiko-testing/stack";
103
102
 
104
103
  import { incidentEntity, incidentTable, webhookDemoFeature } from "../feature";
105
104
 
@@ -102,19 +102,18 @@ the draft (reopening the wizard starts fresh on step 1).
102
102
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
103
103
  import { createConfigFeature } from "@cosmicdrift/kumiko-bundled-features/config";
104
104
  import {
105
+ createFormDraftFeature,
105
106
  FormDraftHandlers,
106
107
  FormDraftQueries,
107
108
  formDraftEntity,
108
- formDraftFeature,
109
109
  } from "@cosmicdrift/kumiko-bundled-features/form-draft";
110
110
  import type { SaveContext } from "@cosmicdrift/kumiko-framework/engine";
111
+ import { createTestUser, TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
111
112
  import {
112
- createTestUser,
113
113
  setupTestStack,
114
114
  type TestStack,
115
- TestUsers,
116
115
  unsafeCreateEntityTable,
117
- } from "@cosmicdrift/kumiko-framework/stack";
116
+ } from "@cosmicdrift/kumiko-testing/stack";
118
117
  import { listingEntity, listingsFeature } from "../feature";
119
118
 
120
119
  let stack: TestStack;
@@ -131,7 +130,7 @@ const DRAFT_KEY = "listing-wizard";
131
130
  beforeAll(async () => {
132
131
  // form-draft requires the "config" feature (its retention-days setting).
133
132
  stack = await setupTestStack({
134
- features: [listingsFeature, formDraftFeature, createConfigFeature()],
133
+ features: [listingsFeature, createFormDraftFeature(), createConfigFeature()],
135
134
  });
136
135
  await unsafeCreateEntityTable(stack.db, listingEntity);
137
136
  await unsafeCreateEntityTable(stack.db, formDraftEntity);
@@ -60,7 +60,8 @@ import {
60
60
  WORKFLOW_WAITING_TYPE,
61
61
  } from "@cosmicdrift/kumiko-framework/engine";
62
62
  import { append, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
63
- import { setupTestStack, type TestStack, TestUsers } from "@cosmicdrift/kumiko-framework/stack";
63
+ import { TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
64
+ import { setupTestStack, type TestStack } from "@cosmicdrift/kumiko-testing/stack";
64
65
  import { createSuspendedRunFetcher } from "../postgres-resume-loop";
65
66
 
66
67
  let stack: TestStack;
@@ -982,8 +983,11 @@ export function startCronScheduler(
982
983
  ### src/db/queries/suspended-runs.ts
983
984
 
984
985
  ```ts
985
- import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
986
- import { type DbRunner, toPostgresArrayLiteral } from "@cosmicdrift/kumiko-framework/db";
986
+ import {
987
+ asRawClient,
988
+ type DbRunner,
989
+ toPostgresArrayLiteral,
990
+ } from "@cosmicdrift/kumiko-framework/db";
987
991
 
988
992
  export async function selectExpiredSuspensionEvents(
989
993
  db: DbRunner,
@@ -57,7 +57,7 @@ import {
57
57
  defineFeature,
58
58
  validateBoot as validateBootRaw,
59
59
  } from "@cosmicdrift/kumiko-framework/engine";
60
- import { withBootValidatorFixture } from "@cosmicdrift/kumiko-framework/testing";
60
+ import { withBootValidatorFixture } from "@cosmicdrift/kumiko-testing/helpers";
61
61
  import { z } from "zod";
62
62
  import { noteDeskFeature } from "../feature";
63
63
 
@@ -35,6 +35,7 @@
35
35
  | `feature-toggles` | Runtime feature-toggle flip — two toggleable features, one registers a cross-feature hook on the other; disabling either has the documented effect without reboot. | `examples/feature-toggles.md` |
36
36
  | `field-access` | Field-level access control. When entity-level roles aren't granular enough. | `examples/field-access.md` |
37
37
  | `files-post-processing` | The three ways to generate an image variant: declarative field variants, imperative ctx.derivatives.variant() with region-blur, and the public route behind a default-deny predicate. | `examples/files-post-processing.md` |
38
+ | `golden-path` | The smallest complete feature: entity, write handler, event, projection, screen and an integration test. | `examples/golden-path.md` |
38
39
  | `i18n` | Feature-scoped translations. 'Hallo' and 'Hello' in the same app. | `examples/i18n.md` |
39
40
  | `idempotency` | Same request, same result. Dedupes on request ID. | `examples/idempotency.md` |
40
41
  | `inbound-mail-triage` | Inbound-mail consumer pattern: inbound-mail-foundation + in-memory provider, a worker-lane triage job attached to the ingest handler, verified end-to-end through createAllInOneEntrypoint. | `examples/inbound-mail-triage.md` |
@@ -5,8 +5,7 @@
5
5
  // kumiko_event_consumers. Mirrors schema-cli.ts's shape (single runXCli(argv,
6
6
  // out) entry point, own DB connection) so `kumiko-consumer` ships the same
7
7
  // way `kumiko-schema` does.
8
- import { createConnection } from "./db/api.js";
9
- import { dbConnectionOptionsFromEnv } from "./db/connection.js";
8
+ import { createDbConnection, dbConnectionOptionsFromEnv } from "./db/connection.js";
10
9
  import { getConsumerState, restartConsumer } from "./pipeline/index.js";
11
10
  import { ensureTemporalPolyfill } from "./time/index.js";
12
11
  // Single pass: pulls --instance-id (both "--instance-id <id>" and
@@ -80,7 +79,7 @@ export async function runConsumerCli(argv, out) {
80
79
  out.err(" DATABASE_URL not set.");
81
80
  return 1;
82
81
  }
83
- const { db, close } = await createConnection(dbUrl, dbConnectionOptionsFromEnv());
82
+ const { db, close } = createDbConnection({ url: dbUrl, ...dbConnectionOptionsFromEnv() });
84
83
  try {
85
84
  if (sub === "status") {
86
85
  const state = await getConsumerState(db, name, instanceId);
@@ -8,4 +8,4 @@ export { type ActiveKmsWiring, buildPgKmsOptions, type KmsWiring, type KmsWiring
8
8
  export { createPgKmsAdapter, PgKmsAdapter, type PgKmsAdapterOptions, type RewrapOptions, type RewrapResult, rewrapSubjectKeys, } from "./pg-kms-adapter.js";
9
9
  export { configuredPiiSubjectKms, configurePiiSubjectKms, decryptPiiFieldValues, decryptPiiValueForSubject, type EncryptPiiOptions, encryptPiiFieldValues, encryptPiiJsonValueForSubject, encryptPiiValueForSubject, isPiiCiphertext, PII_CIPHERTEXT_PREFIX, PII_CIPHERTEXT_PREFIX_JSON, PII_ERASED_SENTINEL, } from "./pii-field-encryption.js";
10
10
  export { createRequestKmsCache, type RequestKmsCache, } from "./request-kms-cache.js";
11
- export { collectPiiEncryptedFields, collectPiiSubjectFields, collectSearchableSubjectFields, type ResolveSubjectOptions, resolveSubjectForField, SubjectResolutionError, } from "./subject-resolver.js";
11
+ export { collectPiiEncryptedFields, collectPiiSubjectFields, collectSearchableSubjectFields, isRecordKeyedField, type ResolveSubjectOptions, resolveSubjectForField, SubjectResolutionError, } from "./subject-resolver.js";
@@ -8,4 +8,4 @@ export { buildPgKmsOptions, requireKmsWiring, requireKmsWiringAsync, resolveKmsW
8
8
  export { createPgKmsAdapter, PgKmsAdapter, rewrapSubjectKeys, } from "./pg-kms-adapter.js";
9
9
  export { configuredPiiSubjectKms, configurePiiSubjectKms, decryptPiiFieldValues, decryptPiiValueForSubject, encryptPiiFieldValues, encryptPiiJsonValueForSubject, encryptPiiValueForSubject, isPiiCiphertext, PII_CIPHERTEXT_PREFIX, PII_CIPHERTEXT_PREFIX_JSON, PII_ERASED_SENTINEL, } from "./pii-field-encryption.js";
10
10
  export { createRequestKmsCache, } from "./request-kms-cache.js";
11
- export { collectPiiEncryptedFields, collectPiiSubjectFields, collectSearchableSubjectFields, resolveSubjectForField, SubjectResolutionError, } from "./subject-resolver.js";
11
+ export { collectPiiEncryptedFields, collectPiiSubjectFields, collectSearchableSubjectFields, isRecordKeyedField, resolveSubjectForField, SubjectResolutionError, } from "./subject-resolver.js";
@@ -12,6 +12,8 @@ export function isPlainUserOwnedField(field) {
12
12
  // owner. Returns the unchanged-by-the-caller values that must be written again
13
13
  // so they encrypt under the new owner. Owner → absent is not a transition:
14
14
  // the value stays with the last owner so that owner's forget still erases it.
15
+ // Absent → owner moves a whenAbsent value (tenant or record key) to the owner's
16
+ // key; the created event stays keyed under the whenAbsent subject.
15
17
  export function personalFieldsRekeyedOnOwnerTransition(entity, previous, mergedNew, submittedChanges) {
16
18
  const rekeyed = {};
17
19
  for (const [name, field] of Object.entries(entity.fields)) {
@@ -1,6 +1,6 @@
1
1
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
2
2
  import postgres from "postgres";
3
- import { DEFAULT_DB_CLOSE_TIMEOUT_SECONDS } from "../db/api.js";
3
+ import { DEFAULT_DB_CLOSE_TIMEOUT_SECONDS } from "../db/connection.js";
4
4
  import { KeyAlreadyExistsError, KeyErasedError, KeyNotFoundError, subjectIdToKey, } from "./kms-adapter.js";
5
5
  const IV_LENGTH = 12;
6
6
  const TAG_LENGTH = 16;
@@ -28,6 +28,8 @@ export interface EventSubjectEnvelope {
28
28
  readonly aggregateId: string;
29
29
  }
30
30
  export declare function resolveEventSubject(fieldName: string, spec: EventPiiSubject, payload: Record<string, unknown>, envelope: EventSubjectEnvelope): SubjectId | null;
31
+ /** True when the field can be keyed on the row's own `record:<entity>:<id>` subject. */
32
+ export declare function isRecordKeyedField(field: EntityDefinition["fields"][string]): boolean;
31
33
  export declare function collectPiiSubjectFields(entity: EntityDefinition): readonly string[];
32
34
  export declare function collectPiiEncryptedFields(entity: EntityDefinition): readonly string[];
33
35
  export declare function collectPiiFileReferenceFields(entity: EntityDefinition): readonly string[];
@@ -34,10 +34,13 @@ function resolveUserOwnedSubject(userOwned, fieldName, row, opts) {
34
34
  const userId = nonEmptyString(row[ownerField]) ?? nonEmptyString(opts.ownerFallback?.[ownerField]);
35
35
  if (userId !== null)
36
36
  return { kind: "user", userId };
37
- if (userOwned.whenAbsent === "tenant" && opts.ignoreWhenAbsent !== true) {
38
- return resolveTenantSubject(fieldName, row, opts);
37
+ if (opts.ignoreWhenAbsent !== true) {
38
+ if (userOwned.whenAbsent === "tenant")
39
+ return resolveTenantSubject(fieldName, row, opts);
40
+ if (userOwned.whenAbsent === "self")
41
+ return resolveRecordSubject(fieldName, row, opts);
39
42
  }
40
- throw new SubjectResolutionError(fieldName, `owner field "${ownerField}" is empty on the row and the field declares no whenAbsent — declare { personal: { of: "${ownerField}", whenAbsent: "tenant" } } (fw#3654)`);
43
+ throw new SubjectResolutionError(fieldName, `owner field "${ownerField}" is empty on the row and the field declares no whenAbsent — declare { personal: { of: "${ownerField}", whenAbsent: "tenant" } } or whenAbsent: "self" to key it on the row itself (fw#3654)`);
41
44
  }
42
45
  function resolveTenantSubject(fieldName, row, opts) {
43
46
  const tenantId = nonEmptyString(row["tenantId"]) ?? opts.tenantId;
@@ -87,6 +90,19 @@ function resolveEnvelopeTenant(fieldName, envelope) {
87
90
  }
88
91
  return { kind: "tenant", tenantId: envelope.tenantId };
89
92
  }
93
+ function resolveEnvelopeRecordSubject(fieldName, envelope) {
94
+ if (nonEmptyString(envelope.aggregateType) === null ||
95
+ nonEmptyString(envelope.aggregateId) === null) {
96
+ throw new SubjectResolutionError(fieldName, "event envelope aggregateType/aggregateId is empty");
97
+ }
98
+ // aggregate_type is a free-form text column (events-schema.ts) — reject
99
+ // here rather than mint a record key that subjectIdSchema (kms-adapter.ts)
100
+ // would later refuse to shred.
101
+ if (!RECORD_ENTITY_PATTERN.test(envelope.aggregateType)) {
102
+ throw new SubjectResolutionError(fieldName, `event aggregateType "${envelope.aggregateType}" does not match ${RECORD_ENTITY_PATTERN} — not a shreddable record subject`);
103
+ }
104
+ return { kind: "record", entity: envelope.aggregateType, id: envelope.aggregateId };
105
+ }
90
106
  // Same resolver for the live-append path (encryptEventPayloadPii) and the
91
107
  // backfill catalog path (backfillEventPiiEncryption) — the reason the two
92
108
  // can never encrypt the same field under different subjects. A "user"
@@ -101,34 +117,33 @@ export function resolveEventSubject(fieldName, spec, payload, envelope) {
101
117
  return { kind: "user", userId };
102
118
  if (normalized.whenAbsent === "tenant")
103
119
  return resolveEnvelopeTenant(fieldName, envelope);
120
+ if (normalized.whenAbsent === "self")
121
+ return resolveEnvelopeRecordSubject(fieldName, envelope);
104
122
  if (normalized.whenAbsent === "plaintext") {
105
123
  // skip: author declared whenAbsent: "plaintext" — value ships unencrypted by decision
106
124
  return null;
107
125
  }
108
126
  throw new SubjectResolutionError(fieldName, `owner field "${normalized.ownerField}" carries no id and the event declares no whenAbsent fallback — ` +
109
127
  `refusing to append plaintext PII. Declare { personal: { of: "${normalized.ownerField}", ` +
110
- `whenAbsent: "tenant" } } to encrypt under the envelope tenant key, or whenAbsent: "plaintext" ` +
128
+ `whenAbsent: "tenant" } } to encrypt under the envelope tenant key, whenAbsent: "self" to encrypt ` +
129
+ `under the event's own record key, or whenAbsent: "plaintext" ` +
111
130
  "to acknowledge that this value cannot be crypto-shredded (fw#2776).");
112
131
  }
113
132
  if (normalized.kind === "tenant") {
114
133
  return resolveEnvelopeTenant(fieldName, envelope);
115
134
  }
116
135
  if (normalized.kind === "self") {
117
- if (nonEmptyString(envelope.aggregateType) === null ||
118
- nonEmptyString(envelope.aggregateId) === null) {
119
- throw new SubjectResolutionError(fieldName, "event envelope aggregateType/aggregateId is empty");
120
- }
121
- // aggregate_type is a free-form text column (events-schema.ts) — reject
122
- // here rather than mint a record key that subjectIdSchema (kms-adapter.ts)
123
- // would later refuse to shred.
124
- if (!RECORD_ENTITY_PATTERN.test(envelope.aggregateType)) {
125
- throw new SubjectResolutionError(fieldName, `event aggregateType "${envelope.aggregateType}" does not match ${RECORD_ENTITY_PATTERN} — not a shreddable record subject`);
126
- }
127
- return { kind: "record", entity: envelope.aggregateType, id: envelope.aggregateId };
136
+ return resolveEnvelopeRecordSubject(fieldName, envelope);
128
137
  }
129
138
  const exhaustiveCheck = normalized;
130
139
  throw new Error(`Unhandled normalized event PII subject: ${JSON.stringify(exhaustiveCheck)}`);
131
140
  }
141
+ /** True when the field can be keyed on the row's own `record:<entity>:<id>` subject. */
142
+ export function isRecordKeyedField(field) {
143
+ if ("recordOwned" in field && field.recordOwned === true)
144
+ return true;
145
+ return "userOwned" in field && field.userOwned?.whenAbsent === "self";
146
+ }
132
147
  function hasSubjectAnnotation(field) {
133
148
  return (("userOwned" in field && field.userOwned !== undefined) ||
134
149
  ("tenantOwned" in field && field.tenantOwned === true) ||
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @deprecated Moved to `@cosmicdrift/kumiko-framework/db`. Run `bun kumiko-upgrade --apply`
3
+ * to rewrite imports. Removed in the first release after 2026-10-29.
4
+ */
5
+ export type { DeleteManyBatchedOptions, DeleteManyBatchedResult, IncrementCounterOptions, InnerJoinRow, InnerJoinSpec, InsertOnConflictDoNothingOptions, JoinColumnPair, JsonTextMatch, SelectOptions, TableInfo, UpsertOnConflictOptions, WhereObject, WhereOperator, WhereValue, } from "./query.js";
6
+ /**
7
+ * @deprecated Moved to `@cosmicdrift/kumiko-framework/db`. Run `bun kumiko-upgrade --apply`
8
+ * to rewrite imports. Removed in the first release after 2026-10-29.
9
+ */
10
+ export { aggregateWhere, asEntityTableMeta, asRawClient, countWhere, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, incrementCounter, insertMany, insertOnConflictDoNothing, insertOne, instantToTimestamptzParam, isTimestamptzType, requireEntityTableMeta, runInSavepoint, runInSavepointIfSupported, runnerCanBeginTransaction, selectInnerJoin, selectMany, transaction, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
@@ -1,4 +1,5 @@
1
- // bun-db: Bun.sql-basierte DB-API ohne drizzle.
2
- // Production-Pfad nach drizzle-removal.
3
- export { bunDbConnectionOptionsFromEnv, createBunDbConnection } from "./connection.js";
1
+ /**
2
+ * @deprecated Moved to `@cosmicdrift/kumiko-framework/db`. Run `bun kumiko-upgrade --apply`
3
+ * to rewrite imports. Removed in the first release after 2026-10-29.
4
+ */
4
5
  export { aggregateWhere, asEntityTableMeta, asRawClient, countWhere, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, incrementCounter, insertMany, insertOnConflictDoNothing, insertOne, instantToTimestamptzParam, isTimestamptzType, requireEntityTableMeta, runInSavepoint, runInSavepointIfSupported, runnerCanBeginTransaction, selectInnerJoin, selectMany, transaction, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
@@ -24,7 +24,7 @@ export async function seedConfigValues(seeds, table, entity, registry, db, ciphe
24
24
  return { created, skipped };
25
25
  const systemUser = createSystemUser(SYSTEM_TENANT_ID);
26
26
  const executor = createEventStoreExecutor(table, entity, { entityName: "config-value" });
27
- const tdb = createTenantDb(db, SYSTEM_TENANT_ID, "system");
27
+ const tdb = createTenantDb({ db, tenantId: SYSTEM_TENANT_ID, mode: "system" });
28
28
  for (const seed of seeds) {
29
29
  const keyDef = registry.getConfigKey(seed.key);
30
30
  if (!keyDef) {
@@ -1,12 +1,30 @@
1
1
  import type { DbConnection, PgClient } from "@cosmicdrift/kumiko-types/db-connection";
2
+ import postgres from "postgres";
2
3
  export type * from "@cosmicdrift/kumiko-types/db-connection";
3
- export type { DbConnectionOptions, DbPoolHandle } from "./api.js";
4
- export { createConnection } from "./api.js";
5
- export declare function createDbConnection(url: string, options?: import("./api.js").DbConnectionOptions): {
4
+ type PostgresDriverOptions = NonNullable<Parameters<typeof postgres>[1]>;
5
+ export type DbConnectionOptions = {
6
+ readonly maxConnections?: number;
7
+ readonly idleTimeoutSeconds?: number;
8
+ readonly connectTimeoutSeconds?: number;
9
+ readonly closeTimeoutSeconds?: number;
10
+ readonly ssl?: PostgresDriverOptions["ssl"];
11
+ readonly onNotice?: (notice: postgres.Notice) => void;
12
+ };
13
+ export type CreateDbConnectionOptions = DbConnectionOptions & {
14
+ readonly url: string;
15
+ };
16
+ export type DbConnectionHandle = {
6
17
  db: DbConnection;
7
18
  client: PgClient;
8
19
  close: () => Promise<void>;
9
20
  };
10
- export declare function dbConnectionOptionsFromEnv(env?: Readonly<Record<string, string | undefined>>): import("./api.js").DbConnectionOptions;
21
+ export declare const DEFAULT_DB_CLOSE_TIMEOUT_SECONDS = 5;
22
+ export declare function createDbConnection(options: CreateDbConnectionOptions): DbConnectionHandle;
23
+ /**
24
+ * @deprecated Positional form, use `createDbConnection({ url, ... })`. Run
25
+ * `bun kumiko-upgrade --apply` to rewrite call sites. Removed in the first release after 2026-10-29.
26
+ */
27
+ export declare function createDbConnection(url: string, options?: DbConnectionOptions): DbConnectionHandle;
28
+ export declare function dbConnectionOptionsFromEnv(env?: Readonly<Record<string, string | undefined>>): DbConnectionOptions;
11
29
  export declare const DEFAULT_OUTSIDE_TRANSACTION_POOL_MAX = 3;
12
- export declare function outsideTransactionPoolOptionsFromEnv(env?: Readonly<Record<string, string | undefined>>): import("./api.js").DbConnectionOptions;
30
+ export declare function outsideTransactionPoolOptionsFromEnv(env?: Readonly<Record<string, string | undefined>>): DbConnectionOptions;
@@ -1,10 +1,11 @@
1
1
  import postgres from "postgres";
2
2
  import { readPositiveIntEnv } from "../utils/env-parse.js";
3
- import { DEFAULT_DB_CLOSE_TIMEOUT_SECONDS } from "./api.js";
4
- export { createConnection } from "./api.js";
5
- // Legacy: postgres-js only. Neue Aufrufer: createConnection() aus api.ts.
6
- // guard:dup-ok — andere Layer als createPgConnection (gibt DbConnection zurück, nicht postgres-Instanz)
7
- export function createDbConnection(url, options = {}) {
3
+ // A DB outage can leave an in-flight query referenced on a dead connection,
4
+ // so a plain end() (no timeout = wait for queries to finish) hangs forever —
5
+ // close() must always bound how long shutdown waits.
6
+ export const DEFAULT_DB_CLOSE_TIMEOUT_SECONDS = 5;
7
+ export function createDbConnection(urlOrOptions, legacyOptions = {}) {
8
+ const { url, ...options } = typeof urlOrOptions === "string" ? { ...legacyOptions, url: urlOrOptions } : urlOrOptions;
8
9
  const pgOptions = {};
9
10
  if (options.maxConnections !== undefined)
10
11
  pgOptions.max = options.maxConnections;
@@ -13,6 +14,10 @@ export function createDbConnection(url, options = {}) {
13
14
  if (options.connectTimeoutSeconds !== undefined) {
14
15
  pgOptions.connect_timeout = options.connectTimeoutSeconds;
15
16
  }
17
+ if (options.ssl !== undefined)
18
+ pgOptions.ssl = options.ssl;
19
+ if (options.onNotice !== undefined)
20
+ pgOptions.onnotice = options.onNotice;
16
21
  const client = postgres(url, pgOptions);
17
22
  return {
18
23
  db: client,
@@ -251,7 +251,7 @@ export function primaryKey(opts) {
251
251
  // Limits: no nested SqlExpression composition (drizzle's recursive
252
252
  // `sql\`${other}\``) — schema-files use single-level expressions only.
253
253
  // Unforgeable via JSON — a client-supplied jsonb value can fake `kind:
254
- // "sql-expr"` but can never carry a Symbol, so isSqlExpression() (bun-db/query.ts)
254
+ // "sql-expr"` but can never carry a Symbol, so isSqlExpression() (db/query.ts)
255
255
  // can't be tricked into treating request data as a raw SQL literal. The same
256
256
  // brand gate is enforced here in the schema DSL (sql`...` interpolation +
257
257
  // literalDefault) and in entity-table-meta's sqlExpressionText — a duck-typed
@@ -3,6 +3,11 @@ import { type Logger } from "../logging/index.js";
3
3
  import type { EnvelopeCipher } from "../secrets/envelope-cipher.js";
4
4
  export declare function collectEncryptedFieldNames(entity: EntityDefinition): ReadonlySet<string>;
5
5
  export declare function isSensitiveLabelField(entity: EntityDefinition, field: string): boolean;
6
+ export declare function isListFilterAllowed(entity: EntityDefinition, field: string, filter: {
7
+ readonly op: string;
8
+ readonly value: unknown;
9
+ }): boolean;
10
+ export declare function isListSortAllowed(entity: EntityDefinition, field: string): boolean;
6
11
  export declare function hasSearchablePlaintext(entity: EntityDefinition, field: string): boolean;
7
12
  export declare function encryptEntityFieldValues(row: Record<string, unknown>, encryptedFields: ReadonlySet<string>, cipher: EnvelopeCipher, opts?: {
8
13
  onlyKeys?: Iterable<string>;
@@ -22,6 +22,35 @@ export function isSensitiveLabelField(entity, field) {
22
22
  return (collectEncryptedFieldNames(entity).has(field) ||
23
23
  collectPiiEncryptedFields(entity).includes(field));
24
24
  }
25
+ // A `sensitive` (find: "secret") field is never comparable either, even while its value is still
26
+ // legacy plaintext.
27
+ function holdsCiphertextOrSecret(entity, field) {
28
+ const def = entity.fields[field];
29
+ if (def === undefined)
30
+ return false;
31
+ return ("sensitive" in def && def.sensitive === true) || isSensitiveLabelField(entity, field);
32
+ }
33
+ // Only a PII field with a blind index (`find: "exact" | "fuzzy"`) can be matched without reading
34
+ // the stored value: the `<field>Bidx` HMAC arm of an equality filter.
35
+ function isBlindIndexEqualityTarget(entity, field) {
36
+ const def = entity.fields[field];
37
+ return (def?.type === "text" &&
38
+ def.lookupable === true &&
39
+ def.sensitive !== true &&
40
+ !collectEncryptedFieldNames(entity).has(field));
41
+ }
42
+ // Filtering ciphertext (or a secret) turns hit/count into a value oracle, and for legacy plaintext
43
+ // rows an `in`/range comparison into a prefix oracle. Blind-index equality is the one exception.
44
+ export function isListFilterAllowed(entity, field, filter) {
45
+ if (!holdsCiphertextOrSecret(entity, field))
46
+ return true;
47
+ return (isBlindIndexEqualityTarget(entity, field) &&
48
+ filter.op === "eq" &&
49
+ typeof filter.value === "string");
50
+ }
51
+ export function isListSortAllowed(entity, field) {
52
+ return !holdsCiphertextOrSecret(entity, field);
53
+ }
25
54
  // `searchable: true` on a text field means it is indexed plaintext
26
55
  // somewhere findable by term — a plain text field's own column, or (for a
27
56
  // personal-data field) the derived search index mirroring it.
@@ -15,7 +15,7 @@
15
15
  // entity-shaped base columns. Escape hatch: no forced audit trail for the
16
16
  // hand-built path; app author owns tenant scoping / versioning. Prefer a
17
17
  // `store_` table name — `read_` is reserved for managed projections (#1208/#1220).
18
- import { collectPiiEncryptedFields } from "../crypto/index.js";
18
+ import { collectPiiEncryptedFields, isRecordKeyedField } from "../crypto/index.js";
19
19
  import { isSqlExpression } from "./dialect.js";
20
20
  import { READ_MODEL_PREFIX, SOFT_DELETE_LIVE_ROW_PREDICATE, toSnakeCase, toTableName, } from "./table-builder.js";
21
21
  // Standard base columns for event-sourced read-model tables. Mirrors
@@ -224,11 +224,12 @@ export function deriveEntityTableMeta(entityName, entity, options) {
224
224
  const idType = entity.idType ?? "uuid";
225
225
  if (source === "unmanaged" && idType === "serial") {
226
226
  // Unmanaged direct-write stores bypass validateRecordOwnedSubjects (it only walks
227
- // feature.entities), yet encryptForDirectWrite still keys recordOwned fields as
228
- // `record:<entity>:<id>`; forgetSubject requires a UUID, so the value could never be shredded.
229
- const recordOwnedField = Object.entries(entity.fields).find(([, field]) => "recordOwned" in field && field.recordOwned === true);
227
+ // feature.entities), yet encryptForDirectWrite still keys recordOwned and
228
+ // whenAbsent: "self" fields as `record:<entity>:<id>`; forgetSubject requires a UUID,
229
+ // so the value could never be shredded.
230
+ const recordOwnedField = Object.entries(entity.fields).find(([, field]) => isRecordKeyedField(field));
230
231
  if (recordOwnedField) {
231
- throw new Error(`deriveEntityTableMeta("${entityName}", { source: "unmanaged" }): field "${recordOwnedField[0]}" is recordOwned but the entity declares idType: "serial" — the record subject key is "record:${entityName}:<id>" and forgetSubject validates that id as a UUID, so this field could never be shredded. Use idType: "uuid", or give the field a different personal stance.`);
232
+ throw new Error(`deriveEntityTableMeta("${entityName}", { source: "unmanaged" }): field "${recordOwnedField[0]}" is recordOwned (or whenAbsent: "self") but the entity declares idType: "serial" — the record subject key is "record:${entityName}:<id>" and forgetSubject validates that id as a UUID, so this field could never be shredded. Use idType: "uuid", or give the field a different personal stance.`);
232
233
  }
233
234
  }
234
235
  // Base columns first, then user-fields. User-fields with the same
@@ -5,12 +5,12 @@ import { executeRawQueryRead } from "../db/queries/raw-sql.js";
5
5
  import { coerceRow, extractTableInfo } from "../db/query.js";
6
6
  import { buildOwnershipClause, combineClauses, normalizeAccessEntry, shiftParams, } from "../engine/ownership.js";
7
7
  import { SYSTEM_TENANT_ID } from "../engine/types/identifiers.js";
8
- import { UnprocessableError } from "../errors/index.js";
8
+ import { UnprocessableError, ValidationError } from "../errors/index.js";
9
9
  import { getStreamVersion } from "../event-store/index.js";
10
10
  import { LIST_ROW_META_REFERENCES } from "../ui-types/list-row-meta.js";
11
11
  import { rehydrateCompoundTypes } from "./compound-types.js";
12
12
  import { decodeKeysetCursor, encodeCursor, encodeKeysetCursor } from "./cursor.js";
13
- import { hasSearchablePlaintext, isSensitiveLabelField } from "./entity-field-encryption.js";
13
+ import { hasSearchablePlaintext, isListFilterAllowed, isListSortAllowed, isSensitiveLabelField, } from "./entity-field-encryption.js";
14
14
  import { buildFilterWhere, } from "./event-store-executor-context.js";
15
15
  import { buildParentRefClause } from "./parent-ref-clause.js";
16
16
  import { buildEntityTable, physicalColumnName } from "./table-builder.js";
@@ -144,7 +144,7 @@ function applyScreenFilter(table, colSql, whereSql, params, f) {
144
144
  return;
145
145
  }
146
146
  for (const [field, value] of Object.entries(screen)) {
147
- // #2015: `x <> NULL` is never true — mirror buildWhereClause's IS [NOT] NULL handling in bun-db/query.ts.
147
+ // #2015: `x <> NULL` is never true — mirror buildWhereClause's IS [NOT] NULL handling in db/query.ts.
148
148
  if (value === null) {
149
149
  whereSql.push(`${colSql(field)} IS NULL`);
150
150
  }
@@ -176,7 +176,7 @@ function applyScreenFilter(table, colSql, whereSql, params, f) {
176
176
  }
177
177
  else {
178
178
  // Blind-index OR rewrite (#818), lock-step with buildWhereClause
179
- // in bun-db/query.ts — equality on lookupable fields matches the
179
+ // in db/query.ts — equality on lookupable fields matches the
180
180
  // plaintext arm OR the HMAC arm.
181
181
  const bidxKey = configuredBlindIndexKey();
182
182
  if (bidxKey !== undefined && typeof value === "string" && table[`${field}Bidx`]) {
@@ -547,7 +547,12 @@ export function createReadVerbs(ctx) {
547
547
  // tenant filter that follows.
548
548
  whereSql.push(`(${orParts.join(" OR ")})`);
549
549
  }
550
- const sortCandidate = payload.sort && table[payload.sort] && fieldReadClause(payload.sort).kind === "pass"
550
+ // A column sort must not 400 (the renderer sends one per header click), and the order of
551
+ // ciphertext carries no meaning for the user while leaking the order of the plaintext.
552
+ const sortCandidate = payload.sort &&
553
+ table[payload.sort] &&
554
+ fieldReadClause(payload.sort).kind === "pass" &&
555
+ isListSortAllowed(entity, payload.sort)
551
556
  ? payload.sort
552
557
  : undefined;
553
558
  const sortDescending = payload.sortDirection === "desc";
@@ -606,6 +611,22 @@ export function createReadVerbs(ctx) {
606
611
  // itself confirm the field exists (field-access.ts strips silently)
607
612
  return;
608
613
  }
614
+ if (!isListFilterAllowed(entity, f.field, f)) {
615
+ throw new ValidationError({
616
+ fields: [
617
+ {
618
+ path: `filter.${f.field}`,
619
+ code: "field_not_filterable",
620
+ i18nKey: "errors.validation.invalid_value",
621
+ params: {
622
+ field: f.field,
623
+ op: f.op,
624
+ hint: "Encrypted, personal and secret fields cannot be filtered; only equality on a field with find: exact or fuzzy.",
625
+ },
626
+ },
627
+ ],
628
+ });
629
+ }
609
630
  if (readClause.kind === "sql") {
610
631
  const shifted = shiftParams({ sqlText: readClause.sqlText, params: readClause.params }, params.length);
611
632
  whereSql.push(shifted.sqlText);
@@ -3,7 +3,7 @@ export { nullBlindIndexesForSubject, recordEventsOwnedExclusivelyByTenant, recor
3
3
  export { collectTableMetas } from "./collect-table-metas.js";
4
4
  export { flattenCompoundTypes, rehydrateCompoundTypes } from "./compound-types.js";
5
5
  export { seedConfigValues } from "./config-seed.js";
6
- export type { DbConnection, DbConnectionOptions, DbPoolHandle, DbRow, DbRunner, DbTx, } from "./connection.js";
6
+ export type { CreateDbConnectionOptions, DbConnection, DbConnectionHandle, DbConnectionOptions, DbRow, DbRunner, DbTx, } from "./connection.js";
7
7
  export { createDbConnection, DEFAULT_OUTSIDE_TRANSACTION_POOL_MAX, dbConnectionOptionsFromEnv, outsideTransactionPoolOptionsFromEnv, } from "./connection.js";
8
8
  export type { CursorQueryOptions, CursorResult, DecodedKeysetCursor } from "./cursor.js";
9
9
  export { decodeCursor, decodeKeysetCursor, encodeCursor, encodeKeysetCursor } from "./cursor.js";
@@ -26,8 +26,8 @@ export { constraintOf, extractPgError, isTableAlreadyExists, isUniqueViolation,
26
26
  export { acquireNamespacedAdvisoryLock } from "./queries/advisory-lock.js";
27
27
  export { toPostgresArrayLiteral } from "./queries/postgres-array-literal.js";
28
28
  export { executeRawQuery, executeRawQueryRead } from "./queries/raw-sql.js";
29
- export type { SelectOptions, WhereObject, WhereValue } from "./query-api.js";
30
- export { aggregateWhere, asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "./query-api.js";
29
+ export type { DeleteManyBatchedOptions, DeleteManyBatchedResult, IncrementCounterOptions, InnerJoinRow, InnerJoinSpec, InsertOnConflictDoNothingOptions, JoinColumnPair, JsonTextMatch, SelectOptions, TableInfo, UpsertOnConflictOptions, WhereObject, WhereOperator, WhereValue, } from "./query.js";
30
+ export { aggregateWhere, asEntityTableMeta, asRawClient, countWhere, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, incrementCounter, insertMany, insertOnConflictDoNothing, insertOne, instantToTimestamptzParam, isTimestamptzType, requireEntityTableMeta, runInSavepoint, runInSavepointIfSupported, runnerCanBeginTransaction, selectInnerJoin, selectMany, transaction, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
31
31
  export { readRebuildMarker, rebuildTablesFromDiff, writeRebuildMarker, } from "./rebuild-marker.js";
32
32
  export { seedReferenceData } from "./reference-data.js";
33
33
  export { renderTableDdl, renderTablesDdl } from "./render-ddl.js";
@@ -35,5 +35,5 @@ export { diffReplayAgainstSnapshot, type ReplayedSchema, type ReplayedTable, typ
35
35
  export { findCommentedDropTables, isRetiredFrameworkTable, type RetiredFrameworkTable, retiredFrameworkTables, } from "./retired-framework-tables.js";
36
36
  export { tableExists } from "./schema-inspection.js";
37
37
  export { buildBaseColumns, buildEntityTable, declareGlobalTenancy, type EntityTable, physicalColumnName, toSnakeCase, toTableName, } from "./table-builder.js";
38
- export type { TenantDb, TenantDbGrants, TenantDbMode, UncheckedSystemDb } from "./tenant-db.js";
38
+ export type { CreateTenantDbOptions, TenantDb, TenantDbGrants, TenantDbMode, UncheckedSystemDb, } from "./tenant-db.js";
39
39
  export { castTenantRows, createSystemDbView, createTenantDb, runInOwnTransaction, SYSTEM_SCOPE_CHECK_BRAND, scopeTenantDbToTenant, } from "./tenant-db.js";
package/dist/db/index.js CHANGED
@@ -20,7 +20,7 @@ export { constraintOf, extractPgError, isTableAlreadyExists, isUniqueViolation,
20
20
  export { acquireNamespacedAdvisoryLock } from "./queries/advisory-lock.js";
21
21
  export { toPostgresArrayLiteral } from "./queries/postgres-array-literal.js";
22
22
  export { executeRawQuery, executeRawQueryRead } from "./queries/raw-sql.js";
23
- export { aggregateWhere, asRawClient, countWhere, deleteMany, fetchOne, insertMany, insertOne, runInSavepoint, selectMany, transaction, updateMany, } from "./query-api.js";
23
+ export { aggregateWhere, asEntityTableMeta, asRawClient, countWhere, deleteMany, deleteManyBatched, extractTableInfo, fetchOne, incrementCounter, insertMany, insertOnConflictDoNothing, insertOne, instantToTimestamptzParam, isTimestamptzType, requireEntityTableMeta, runInSavepoint, runInSavepointIfSupported, runnerCanBeginTransaction, selectInnerJoin, selectMany, transaction, unsafeReadRetrying, updateMany, upsertByPk, upsertOnConflict, } from "./query.js";
24
24
  export { readRebuildMarker, rebuildTablesFromDiff, writeRebuildMarker, } from "./rebuild-marker.js";
25
25
  export { seedReferenceData } from "./reference-data.js";
26
26
  export { renderTableDdl, renderTablesDdl } from "./render-ddl.js";
@@ -17,7 +17,7 @@ import { createHash } from "node:crypto";
17
17
  import { existsSync, readdirSync, readFileSync } from "node:fs";
18
18
  import { join } from "node:path";
19
19
  import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
20
- import { asRawClient, hasBegin, } from "../bun-db/query.js";
20
+ import { asRawClient, hasBegin, } from "./query.js";
21
21
  // Advisory-lock key (random 32-bit int, stable). Verhindert dass zwei
22
22
  // gleichzeitig bootende Pods beide migrate apply laufen lassen (zweiter
23
23
  // blockiert bis erster fertig).