@openhi/constructs 0.0.175 → 0.0.177

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.
@@ -59,10 +59,10 @@ function buildResourceSoftDeleteSql(schemaName) {
59
59
  `UPDATE ${s}.resources`,
60
60
  `SET deleted_at = $6,`,
61
61
  ` version = $5`,
62
- `WHERE tenant_id = $1`,
63
- ` AND workspace_id = $2`,
62
+ `WHERE LOWER(tenant_id) = LOWER($1)`,
63
+ ` AND LOWER(workspace_id) = LOWER($2)`,
64
64
  ` AND LOWER(resource_type) = LOWER($3)`,
65
- ` AND resource_id = $4`,
65
+ ` AND LOWER(resource_id) = LOWER($4)`,
66
66
  ` AND $5 > version;`
67
67
  ].join("\n");
68
68
  }
@@ -76,4 +76,4 @@ export {
76
76
  buildResourceSoftDeleteSql,
77
77
  ensureSchemaBootstrap
78
78
  };
79
- //# sourceMappingURL=chunk-XJ5SRUGN.mjs.map
79
+ //# sourceMappingURL=chunk-B6GCDQKX.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/postgres/data-store-postgres-schema.ts"],"sourcesContent":["import type { Pool, PoolClient } from \"pg\";\n\n/**\n * @see sites/www-docs/content/architecture/adr/2026-04-17-01-ad-hoc-query-support-fhir-api.md\n *\n * SQL strings for the Postgres replication tier (ADR 2026-04-17-01, phase 1):\n * the JSONB `resources` table that mirrors current FHIR resources from the\n * DynamoDB single-table store. Phase 1 covers replication only; query routing\n * and indexes for individual SearchParameters are deferred to a follow-on phase.\n *\n * The `version` column stores the resource's `vid` (a ULID — see\n * `data-entity-common.ts`). ULIDs are monotonically lexically sortable, so the\n * UPSERT/UPDATE guards use plain `>` text comparison to reject out-of-order\n * Kinesis deliveries.\n */\n\nexport const POSTGRES_REPLICATION_SCHEMA_VERSION = 1;\n\nconst SCHEMA_NAME_PATTERN = /^[a-z_][a-z0-9_]{0,62}$/;\n\n/**\n * Validate that a schema name is a safe Postgres identifier and quote it for\n * inclusion in DDL/DML. Throws on anything that doesn't match the lower-snake\n * pattern OpenHI uses for branch-derived schema names (e.g. `b_a1b2c3`).\n */\nexport function quoteSchemaIdentifier(schemaName: string): string {\n if (!SCHEMA_NAME_PATTERN.test(schemaName)) {\n throw new Error(\n `Invalid Postgres schema name: ${JSON.stringify(schemaName)}; expected /[a-z_][a-z0-9_]{0,62}/`,\n );\n }\n return `\"${schemaName}\"`;\n}\n\n/**\n * Build the bootstrap DDL as a list of independent statements. Use this form\n * when sending the DDL through a transport that does not accept multi-statement\n * SQL in a single call — most importantly the AWS RDS Data API's\n * `ExecuteStatement`, which the REST API runner uses.\n *\n * Each statement is fully self-contained and idempotent (`IF NOT EXISTS`),\n * so the array can be executed in order with no transaction wrapper.\n */\nexport function buildSchemaBootstrapStatements(\n schemaName: string,\n): ReadonlyArray<string> {\n const s = quoteSchemaIdentifier(schemaName);\n return [\n `CREATE SCHEMA IF NOT EXISTS ${s};`,\n [\n `CREATE TABLE IF NOT EXISTS ${s}.resources (`,\n ` tenant_id text NOT NULL,`,\n ` workspace_id text NOT NULL,`,\n ` resource_type text NOT NULL,`,\n ` resource_id text NOT NULL,`,\n ` version text NOT NULL,`,\n ` last_updated timestamptz NOT NULL,`,\n ` deleted_at timestamptz,`,\n ` resource jsonb NOT NULL,`,\n ` PRIMARY KEY (tenant_id, workspace_id, resource_type, resource_id)`,\n `);`,\n ].join(\"\\n\"),\n [\n `CREATE INDEX IF NOT EXISTS resources_jsonb_gin`,\n ` ON ${s}.resources USING gin (resource);`,\n ].join(\"\\n\"),\n [\n `CREATE INDEX IF NOT EXISTS resources_listing`,\n ` ON ${s}.resources (tenant_id, workspace_id, resource_type, last_updated);`,\n ].join(\"\\n\"),\n ];\n}\n\nexport function buildSchemaBootstrapSql(schemaName: string): string {\n return buildSchemaBootstrapStatements(schemaName).join(\"\\n\");\n}\n\n/**\n * INSERT/MODIFY UPSERT with a monotonic `version` (ULID) guard. If a record\n * arrives out of order — e.g. a retried Kinesis delivery for an older `vid`\n * after a newer one — the WHERE clause in DO UPDATE leaves the row unchanged.\n *\n * Param order: $1 tenant_id, $2 workspace_id, $3 resource_type, $4 resource_id,\n * $5 version, $6 last_updated, $7 resource (jsonb-serialized).\n */\nexport function buildResourceUpsertSql(schemaName: string): string {\n const s = quoteSchemaIdentifier(schemaName);\n return [\n `INSERT INTO ${s}.resources`,\n ` (tenant_id, workspace_id, resource_type, resource_id, version, last_updated, deleted_at, resource)`,\n `VALUES ($1, $2, $3, $4, $5, $6, NULL, $7::jsonb)`,\n `ON CONFLICT (tenant_id, workspace_id, resource_type, resource_id)`,\n `DO UPDATE SET`,\n ` version = EXCLUDED.version,`,\n ` last_updated = EXCLUDED.last_updated,`,\n ` deleted_at = NULL,`,\n ` resource = EXCLUDED.resource`,\n `WHERE EXCLUDED.version > ${s}.resources.version;`,\n ].join(\"\\n\");\n}\n\n/**\n * REMOVE soft-delete with the same monotonic `version` guard. Using `>` (not\n * `>=`) means a duplicate REMOVE for the same version is a no-op. Param order\n * matches {@link buildResourceUpsertSql} for the first five params.\n *\n * Param order: $1 tenant_id, $2 workspace_id, $3 resource_type, $4 resource_id,\n * $5 version (incoming), $6 deleted_at.\n */\nexport function buildResourceSoftDeleteSql(schemaName: string): string {\n const s = quoteSchemaIdentifier(schemaName);\n // `resource_type` is matched case-insensitively so a REMOVE event whose\n // `keys.resourceType` resolves to a different case than the existing row\n // still hits the row. This protects the soft-delete during the transition\n // window after the upstream PK-fallback bug is fixed and new writes start\n // landing as canonical FHIR PascalCase while old rows remain lowercase.\n return [\n `UPDATE ${s}.resources`,\n `SET deleted_at = $6,`,\n ` version = $5`,\n `WHERE tenant_id = $1`,\n ` AND workspace_id = $2`,\n ` AND LOWER(resource_type) = LOWER($3)`,\n ` AND resource_id = $4`,\n ` AND $5 > version;`,\n ].join(\"\\n\");\n}\n\n/**\n * Run schema DDL idempotently. Safe to call from every Lambda cold start; the\n * `IF NOT EXISTS` clauses make repeats no-ops. Caller is responsible for\n * memoizing across invocations on the same warm container.\n */\nexport async function ensureSchemaBootstrap(\n client: Pool | PoolClient,\n schemaName: string,\n): Promise<void> {\n await client.query(buildSchemaBootstrapSql(schemaName));\n}\n"],"mappings":";AAkBA,IAAM,sBAAsB;AAOrB,SAAS,sBAAsB,YAA4B;AAChE,MAAI,CAAC,oBAAoB,KAAK,UAAU,GAAG;AACzC,UAAM,IAAI;AAAA,MACR,iCAAiC,KAAK,UAAU,UAAU,CAAC;AAAA,IAC7D;AAAA,EACF;AACA,SAAO,IAAI,UAAU;AACvB;AAWO,SAAS,+BACd,YACuB;AACvB,QAAM,IAAI,sBAAsB,UAAU;AAC1C,SAAO;AAAA,IACL,+BAA+B,CAAC;AAAA,IAChC;AAAA,MACE,8BAA8B,CAAC;AAAA,MAC/B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX;AAAA,MACE;AAAA,MACA,QAAQ,CAAC;AAAA,IACX,EAAE,KAAK,IAAI;AAAA,IACX;AAAA,MACE;AAAA,MACA,QAAQ,CAAC;AAAA,IACX,EAAE,KAAK,IAAI;AAAA,EACb;AACF;AAEO,SAAS,wBAAwB,YAA4B;AAClE,SAAO,+BAA+B,UAAU,EAAE,KAAK,IAAI;AAC7D;AAUO,SAAS,uBAAuB,YAA4B;AACjE,QAAM,IAAI,sBAAsB,UAAU;AAC1C,SAAO;AAAA,IACL,eAAe,CAAC;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,4BAA4B,CAAC;AAAA,EAC/B,EAAE,KAAK,IAAI;AACb;AAUO,SAAS,2BAA2B,YAA4B;AACrE,QAAM,IAAI,sBAAsB,UAAU;AAM1C,SAAO;AAAA,IACL,UAAU,CAAC;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAOA,eAAsB,sBACpB,QACA,YACe;AACf,QAAM,OAAO,MAAM,wBAAwB,UAAU,CAAC;AACxD;","names":[]}
1
+ {"version":3,"sources":["../src/components/postgres/data-store-postgres-schema.ts"],"sourcesContent":["import type { Pool, PoolClient } from \"pg\";\n\n/**\n * @see sites/www-docs/content/architecture/adr/2026-04-17-01-ad-hoc-query-support-fhir-api.md\n *\n * SQL strings for the Postgres replication tier (ADR 2026-04-17-01, phase 1):\n * the JSONB `resources` table that mirrors current FHIR resources from the\n * DynamoDB single-table store. Phase 1 covers replication only; query routing\n * and indexes for individual SearchParameters are deferred to a follow-on phase.\n *\n * The `version` column stores the resource's `vid` (a ULID — see\n * `data-entity-common.ts`). ULIDs are monotonically lexically sortable, so the\n * UPSERT/UPDATE guards use plain `>` text comparison to reject out-of-order\n * Kinesis deliveries.\n */\n\nexport const POSTGRES_REPLICATION_SCHEMA_VERSION = 1;\n\nconst SCHEMA_NAME_PATTERN = /^[a-z_][a-z0-9_]{0,62}$/;\n\n/**\n * Validate that a schema name is a safe Postgres identifier and quote it for\n * inclusion in DDL/DML. Throws on anything that doesn't match the lower-snake\n * pattern OpenHI uses for branch-derived schema names (e.g. `b_a1b2c3`).\n */\nexport function quoteSchemaIdentifier(schemaName: string): string {\n if (!SCHEMA_NAME_PATTERN.test(schemaName)) {\n throw new Error(\n `Invalid Postgres schema name: ${JSON.stringify(schemaName)}; expected /[a-z_][a-z0-9_]{0,62}/`,\n );\n }\n return `\"${schemaName}\"`;\n}\n\n/**\n * Build the bootstrap DDL as a list of independent statements. Use this form\n * when sending the DDL through a transport that does not accept multi-statement\n * SQL in a single call — most importantly the AWS RDS Data API's\n * `ExecuteStatement`, which the REST API runner uses.\n *\n * Each statement is fully self-contained and idempotent (`IF NOT EXISTS`),\n * so the array can be executed in order with no transaction wrapper.\n */\nexport function buildSchemaBootstrapStatements(\n schemaName: string,\n): ReadonlyArray<string> {\n const s = quoteSchemaIdentifier(schemaName);\n return [\n `CREATE SCHEMA IF NOT EXISTS ${s};`,\n [\n `CREATE TABLE IF NOT EXISTS ${s}.resources (`,\n ` tenant_id text NOT NULL,`,\n ` workspace_id text NOT NULL,`,\n ` resource_type text NOT NULL,`,\n ` resource_id text NOT NULL,`,\n ` version text NOT NULL,`,\n ` last_updated timestamptz NOT NULL,`,\n ` deleted_at timestamptz,`,\n ` resource jsonb NOT NULL,`,\n ` PRIMARY KEY (tenant_id, workspace_id, resource_type, resource_id)`,\n `);`,\n ].join(\"\\n\"),\n [\n `CREATE INDEX IF NOT EXISTS resources_jsonb_gin`,\n ` ON ${s}.resources USING gin (resource);`,\n ].join(\"\\n\"),\n [\n `CREATE INDEX IF NOT EXISTS resources_listing`,\n ` ON ${s}.resources (tenant_id, workspace_id, resource_type, last_updated);`,\n ].join(\"\\n\"),\n ];\n}\n\nexport function buildSchemaBootstrapSql(schemaName: string): string {\n return buildSchemaBootstrapStatements(schemaName).join(\"\\n\");\n}\n\n/**\n * INSERT/MODIFY UPSERT with a monotonic `version` (ULID) guard. If a record\n * arrives out of order — e.g. a retried Kinesis delivery for an older `vid`\n * after a newer one — the WHERE clause in DO UPDATE leaves the row unchanged.\n *\n * Param order: $1 tenant_id, $2 workspace_id, $3 resource_type, $4 resource_id,\n * $5 version, $6 last_updated, $7 resource (jsonb-serialized).\n */\nexport function buildResourceUpsertSql(schemaName: string): string {\n const s = quoteSchemaIdentifier(schemaName);\n return [\n `INSERT INTO ${s}.resources`,\n ` (tenant_id, workspace_id, resource_type, resource_id, version, last_updated, deleted_at, resource)`,\n `VALUES ($1, $2, $3, $4, $5, $6, NULL, $7::jsonb)`,\n `ON CONFLICT (tenant_id, workspace_id, resource_type, resource_id)`,\n `DO UPDATE SET`,\n ` version = EXCLUDED.version,`,\n ` last_updated = EXCLUDED.last_updated,`,\n ` deleted_at = NULL,`,\n ` resource = EXCLUDED.resource`,\n `WHERE EXCLUDED.version > ${s}.resources.version;`,\n ].join(\"\\n\");\n}\n\n/**\n * REMOVE soft-delete with the same monotonic `version` guard. Using `>` (not\n * `>=`) means a duplicate REMOVE for the same version is a no-op. Param order\n * matches {@link buildResourceUpsertSql} for the first five params.\n *\n * Param order: $1 tenant_id, $2 workspace_id, $3 resource_type, $4 resource_id,\n * $5 version (incoming), $6 deleted_at.\n */\nexport function buildResourceSoftDeleteSql(schemaName: string): string {\n const s = quoteSchemaIdentifier(schemaName);\n // Every key column is matched case-insensitively so a REMOVE event whose\n // upstream-derived keys resolve to a different case than the existing row\n // still hits the row. Protects the soft-delete against any pipeline-side\n // casing drift on `tenant_id`, `workspace_id`, `resource_type`, or\n // `resource_id` between writers and removers.\n return [\n `UPDATE ${s}.resources`,\n `SET deleted_at = $6,`,\n ` version = $5`,\n `WHERE LOWER(tenant_id) = LOWER($1)`,\n ` AND LOWER(workspace_id) = LOWER($2)`,\n ` AND LOWER(resource_type) = LOWER($3)`,\n ` AND LOWER(resource_id) = LOWER($4)`,\n ` AND $5 > version;`,\n ].join(\"\\n\");\n}\n\n/**\n * Run schema DDL idempotently. Safe to call from every Lambda cold start; the\n * `IF NOT EXISTS` clauses make repeats no-ops. Caller is responsible for\n * memoizing across invocations on the same warm container.\n */\nexport async function ensureSchemaBootstrap(\n client: Pool | PoolClient,\n schemaName: string,\n): Promise<void> {\n await client.query(buildSchemaBootstrapSql(schemaName));\n}\n"],"mappings":";AAkBA,IAAM,sBAAsB;AAOrB,SAAS,sBAAsB,YAA4B;AAChE,MAAI,CAAC,oBAAoB,KAAK,UAAU,GAAG;AACzC,UAAM,IAAI;AAAA,MACR,iCAAiC,KAAK,UAAU,UAAU,CAAC;AAAA,IAC7D;AAAA,EACF;AACA,SAAO,IAAI,UAAU;AACvB;AAWO,SAAS,+BACd,YACuB;AACvB,QAAM,IAAI,sBAAsB,UAAU;AAC1C,SAAO;AAAA,IACL,+BAA+B,CAAC;AAAA,IAChC;AAAA,MACE,8BAA8B,CAAC;AAAA,MAC/B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,EAAE,KAAK,IAAI;AAAA,IACX;AAAA,MACE;AAAA,MACA,QAAQ,CAAC;AAAA,IACX,EAAE,KAAK,IAAI;AAAA,IACX;AAAA,MACE;AAAA,MACA,QAAQ,CAAC;AAAA,IACX,EAAE,KAAK,IAAI;AAAA,EACb;AACF;AAEO,SAAS,wBAAwB,YAA4B;AAClE,SAAO,+BAA+B,UAAU,EAAE,KAAK,IAAI;AAC7D;AAUO,SAAS,uBAAuB,YAA4B;AACjE,QAAM,IAAI,sBAAsB,UAAU;AAC1C,SAAO;AAAA,IACL,eAAe,CAAC;AAAA,IAChB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,4BAA4B,CAAC;AAAA,EAC/B,EAAE,KAAK,IAAI;AACb;AAUO,SAAS,2BAA2B,YAA4B;AACrE,QAAM,IAAI,sBAAsB,UAAU;AAM1C,SAAO;AAAA,IACL,UAAU,CAAC;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAOA,eAAsB,sBACpB,QACA,YACe;AACf,QAAM,OAAO,MAAM,wBAAwB,UAAU,CAAC;AACxD;","names":[]}
@@ -824,10 +824,10 @@ function buildResourceSoftDeleteSql(schemaName) {
824
824
  `UPDATE ${s}.resources`,
825
825
  `SET deleted_at = $6,`,
826
826
  ` version = $5`,
827
- `WHERE tenant_id = $1`,
828
- ` AND workspace_id = $2`,
827
+ `WHERE LOWER(tenant_id) = LOWER($1)`,
828
+ ` AND LOWER(workspace_id) = LOWER($2)`,
829
829
  ` AND LOWER(resource_type) = LOWER($3)`,
830
- ` AND resource_id = $4`,
830
+ ` AND LOWER(resource_id) = LOWER($4)`,
831
831
  ` AND $5 > version;`
832
832
  ].join("\n");
833
833
  }
@@ -1137,10 +1137,16 @@ function buildWriteIntent(change, awsRegion) {
1137
1137
  if (shouldDropAsGlobalTableReplicationRecord(change, awsRegion)) {
1138
1138
  return { kind: "drop" };
1139
1139
  }
1140
- const keys = parseCurrentResourceKeys(change);
1141
- if (!keys) {
1140
+ const parsed = parseCurrentResourceKeys(change);
1141
+ if (!parsed) {
1142
1142
  return { kind: "drop" };
1143
1143
  }
1144
+ const keys = {
1145
+ ...parsed,
1146
+ tenantId: parsed.tenantId.toLowerCase(),
1147
+ workspaceId: parsed.workspaceId.toLowerCase(),
1148
+ resourceId: parsed.resourceId.toLowerCase()
1149
+ };
1144
1150
  const isRemove = change.eventName === "REMOVE";
1145
1151
  const image = isRemove ? change.dynamodb?.OldImage : change.dynamodb?.NewImage;
1146
1152
  if (!image) {