@cosmicdrift/kumiko-bundled-features 0.45.0 → 0.46.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.45.0",
3
+ "version": "0.46.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -0,0 +1,33 @@
1
+ // Regression for framework#347: the migration generator (collectTableMetas)
2
+ // must see the ride-along columns/indexes that live only on a feature's
3
+ // backing Drizzle table, not just the entity fields. Before the fix the
4
+ // generated migration omitted them → prod-500 (publicstatus#116). Guards the
5
+ // `r.entity(name, def, { table })` wiring on the two real ride-along features
6
+ // so a future removal of the `{ table }` arg fails here.
7
+
8
+ import { describe, expect, test } from "bun:test";
9
+ import { collectTableMetas } from "@cosmicdrift/kumiko-framework/db";
10
+ import { createDeliveryFeature } from "../delivery/feature";
11
+ import { createSecretsFeature } from "../secrets/feature";
12
+
13
+ describe("ride-along schema metas reach the generator (framework#347)", () => {
14
+ test("secrets read_tenant_secrets: envelope/metadata/last_rotated_at + (tenant,key) uniqueIndex", () => {
15
+ const meta = collectTableMetas([createSecretsFeature()]).find(
16
+ (m) => m.tableName === "read_tenant_secrets",
17
+ );
18
+ const cols = meta?.columns.map((c) => c.name) ?? [];
19
+ expect(cols).toContain("envelope");
20
+ expect(cols).toContain("metadata");
21
+ expect(cols).toContain("last_rotated_at");
22
+ expect(meta?.indexes.map((i) => i.name)).toContain("read_tenant_secrets_tenant_key_unique");
23
+ });
24
+
25
+ test("delivery read_notification_preferences: (tenant,user,type,channel) uniqueIndex", () => {
26
+ const meta = collectTableMetas([createDeliveryFeature()]).find(
27
+ (m) => m.tableName === "read_notification_preferences",
28
+ );
29
+ const unique = meta?.indexes.find((i) => i.name === "read_notification_preferences_unique");
30
+ expect(unique).toBeDefined();
31
+ expect(unique?.unique).toBe(true);
32
+ });
33
+ });
@@ -10,6 +10,7 @@ import {
10
10
  deliveryAttemptsTable,
11
11
  deliveryAttemptsTableMeta,
12
12
  notificationPreferenceEntity,
13
+ notificationPreferencesTable,
13
14
  } from "./tables";
14
15
 
15
16
  export function createDeliveryFeature(): FeatureDefinition {
@@ -18,7 +19,12 @@ export function createDeliveryFeature(): FeatureDefinition {
18
19
  "The notification dispatch core: call `ctx.notify(notificationType, { to, route, data, priority, idempotencyKey })` from any handler to fan out a notification across all registered channels (email, in-app, push). It stores per-user channel preferences in the `notification-preference` entity, logs every attempt to `read_delivery_attempts`, and enforces idempotency and rate-limiting \u2014 add `channel-email`, `channel-in-app`, or `channel-push` on top to actually send anything.",
19
20
  );
20
21
  r.systemScope();
21
- r.entity("notification-preference", notificationPreferenceEntity);
22
+ // Backing table: the (tenant,user,type,channel) uniqueIndex lives only on
23
+ // the physical table, not on the entity fields, so the generator would
24
+ // otherwise omit it → duplicate preference rows on concurrent upserts.
25
+ r.entity("notification-preference", notificationPreferenceEntity, {
26
+ table: notificationPreferencesTable,
27
+ });
22
28
  r.unmanagedTable(deliveryAttemptsTableMeta, {
23
29
  reason: "read_side.delivery_attempt_log",
24
30
  });
@@ -7,7 +7,7 @@ import { listQuery } from "./handlers/list.query";
7
7
  import { rotateJob } from "./handlers/rotate.job";
8
8
  import { setWrite } from "./handlers/set.write";
9
9
  import { secretReadSchema } from "./secrets-context";
10
- import { tenantSecretEntity } from "./table";
10
+ import { tenantSecretEntity, tenantSecretsTable } from "./table";
11
11
 
12
12
  /**
13
13
  * Env-vars contract for the `secrets` feature. Apps merge this via
@@ -93,7 +93,11 @@ export function createSecretsFeature(): FeatureDefinition {
93
93
  // .updated/.deleted` events land on the aggregate stream. Reads fire a
94
94
  // separate `tenantSecretRead` event per call (see secrets-context.get
95
95
  // for the one-event-per-read rationale).
96
- r.entity("tenant-secret", tenantSecretEntity);
96
+ // Backing table: envelope/metadata/last_rotated_at + the (tenant,key)
97
+ // uniqueIndex are not expressible via the field-DSL (jsonb-without-default,
98
+ // now()-default), so the physical table is the DDL truth. Without this the
99
+ // generated migration omitted those columns → prod-500 (publicstatus#116).
100
+ r.entity("tenant-secret", tenantSecretEntity, { table: tenantSecretsTable });
97
101
 
98
102
  // Read-audit domain-event. Registered here so ops tools + MSPs can
99
103
  // discover the type; secrets-context.get parses payloads against