@openhi/constructs 0.0.176 → 0.0.178

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.
@@ -9,7 +9,7 @@ import {
9
9
  buildResourceSoftDeleteSql,
10
10
  buildResourceUpsertSql,
11
11
  ensureSchemaBootstrap
12
- } from "./chunk-XJ5SRUGN.mjs";
12
+ } from "./chunk-B6GCDQKX.mjs";
13
13
  import {
14
14
  decompressResource
15
15
  } from "./chunk-APVVG7BO.mjs";
@@ -147,10 +147,16 @@ function buildWriteIntent(change, awsRegion) {
147
147
  if (shouldDropAsGlobalTableReplicationRecord(change, awsRegion)) {
148
148
  return { kind: "drop" };
149
149
  }
150
- const keys = parseCurrentResourceKeys(change);
151
- if (!keys) {
150
+ const parsed = parseCurrentResourceKeys(change);
151
+ if (!parsed) {
152
152
  return { kind: "drop" };
153
153
  }
154
+ const keys = {
155
+ ...parsed,
156
+ tenantId: parsed.tenantId.toLowerCase(),
157
+ workspaceId: parsed.workspaceId.toLowerCase(),
158
+ resourceId: parsed.resourceId.toLowerCase()
159
+ };
154
160
  const isRemove = change.eventName === "REMOVE";
155
161
  const image = isRemove ? change.dynamodb?.OldImage : change.dynamodb?.NewImage;
156
162
  if (!image) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/postgres/data-store-postgres-replication.handler.ts"],"sourcesContent":["import {\n GetSecretValueCommand,\n SecretsManagerClient,\n} from \"@aws-sdk/client-secrets-manager\";\nimport type {\n KinesisStreamBatchResponse,\n KinesisStreamEvent,\n KinesisStreamRecord,\n} from \"aws-lambda\";\nimport { Pool } from \"pg\";\nimport {\n buildResourceSoftDeleteSql,\n buildResourceUpsertSql,\n ensureSchemaBootstrap,\n} from \"./data-store-postgres-schema\";\nimport { decompressResource } from \"../../lib/compression\";\nimport {\n type DynamoDbStreamKinesisRecord,\n dynamodbImageToPlain,\n} from \"../dynamodb/dynamodb-stream-record\";\nimport {\n parseCurrentResourceKeys,\n shouldDropAsGlobalTableReplicationRecord,\n} from \"../dynamodb/firehose-archive-transform.handler\";\n\n/**\n * Postgres replication consumer for the data store change Kinesis stream\n * (ADR 2026-04-17-01, phase 1). Reads the same stream that feeds the\n * Firehose-to-S3 archive and projects each current FHIR resource into the\n * `resources` JSONB table on the Aurora Serverless v2 cluster provisioned by\n * {@link DataStorePostgresReplica}. This phase implements replication only;\n * no read-side query routing is wired up here.\n */\n\nconst REQUIRED_ENV_VARS = [\n \"OPENHI_PG_HOST\",\n \"OPENHI_PG_PORT\",\n \"OPENHI_PG_DATABASE\",\n \"OPENHI_PG_SCHEMA\",\n] as const;\n\ntype RequiredEnvVar = (typeof REQUIRED_ENV_VARS)[number];\n\ninterface PostgresConnectionConfig {\n host: string;\n port: number;\n database: string;\n schema: string;\n user: string;\n password: string;\n ssl: boolean;\n}\n\ninterface SecretsManagerCredentials {\n username: string;\n password: string;\n}\n\nlet cachedConfig: PostgresConnectionConfig | undefined;\nlet pool: Pool | undefined;\nlet bootstrapPromise: Promise<void> | undefined;\nlet secretsClient: SecretsManagerClient | undefined;\n\nfunction readEnv(name: RequiredEnvVar): string {\n const v = process.env[name]?.trim();\n if (!v) {\n throw new Error(\n `Missing required environment variable for postgres replication: ${name}`,\n );\n }\n return v;\n}\n\nasync function loadConfig(): Promise<PostgresConnectionConfig> {\n if (cachedConfig) {\n return cachedConfig;\n }\n const host = readEnv(\"OPENHI_PG_HOST\");\n const port = Number.parseInt(readEnv(\"OPENHI_PG_PORT\"), 10);\n const database = readEnv(\"OPENHI_PG_DATABASE\");\n const schema = readEnv(\"OPENHI_PG_SCHEMA\");\n\n // Test/integration setups can supply user/password directly via env to skip\n // the Secrets Manager round-trip. Production always goes through the secret.\n const directUser = process.env.OPENHI_PG_USER?.trim();\n const directPassword = process.env.OPENHI_PG_PASSWORD;\n if (directUser && directPassword !== undefined) {\n cachedConfig = {\n host,\n port,\n database,\n schema,\n user: directUser,\n password: directPassword,\n ssl: process.env.OPENHI_PG_SSL?.trim() === \"true\",\n };\n return cachedConfig;\n }\n\n const secretArn = process.env.OPENHI_PG_SECRET_ARN?.trim();\n if (!secretArn) {\n throw new Error(\n \"Either OPENHI_PG_SECRET_ARN or both OPENHI_PG_USER and OPENHI_PG_PASSWORD must be set\",\n );\n }\n if (!secretsClient) {\n secretsClient = new SecretsManagerClient({});\n }\n const out = await secretsClient.send(\n new GetSecretValueCommand({ SecretId: secretArn }),\n );\n if (!out.SecretString) {\n throw new Error(\n `Secret ${secretArn} returned no SecretString; binary secrets are not supported`,\n );\n }\n const parsed = JSON.parse(out.SecretString) as SecretsManagerCredentials;\n cachedConfig = {\n host,\n port,\n database,\n schema,\n user: parsed.username,\n password: parsed.password,\n ssl: true,\n };\n return cachedConfig;\n}\n\nasync function getPool(): Promise<Pool> {\n if (pool) {\n return pool;\n }\n const cfg = await loadConfig();\n pool = new Pool({\n host: cfg.host,\n port: cfg.port,\n database: cfg.database,\n user: cfg.user,\n password: cfg.password,\n ssl: cfg.ssl ? { rejectUnauthorized: false } : false,\n max: 2,\n idleTimeoutMillis: 60_000,\n connectionTimeoutMillis: 10_000,\n });\n return pool;\n}\n\nasync function ensureBootstrapped(): Promise<void> {\n if (!bootstrapPromise) {\n bootstrapPromise = (async () => {\n const cfg = await loadConfig();\n const p = await getPool();\n await ensureSchemaBootstrap(p, cfg.schema);\n })().catch((err) => {\n // Reset so a subsequent invocation retries the bootstrap on a fresh\n // container; otherwise a transient failure during cold start would\n // poison every record forever.\n bootstrapPromise = undefined;\n throw err;\n });\n }\n return bootstrapPromise;\n}\n\n/**\n * Reset module-scoped caches. Test-only helper; do not invoke from production\n * code paths. Exposed so unit and integration tests can run multiple\n * scenarios within a single Jest worker without leaking state across cases.\n */\nexport function __resetReplicationModuleStateForTests(): void {\n cachedConfig = undefined;\n pool = undefined;\n bootstrapPromise = undefined;\n secretsClient = undefined;\n}\n\nfunction decodeKinesisRecord(\n record: KinesisStreamRecord,\n): DynamoDbStreamKinesisRecord {\n const json = Buffer.from(record.kinesis.data, \"base64\").toString(\"utf8\");\n return JSON.parse(json) as DynamoDbStreamKinesisRecord;\n}\n\nfunction deriveLastUpdated(\n resource: unknown,\n approximateCreationEpochMs: number | undefined,\n): Date {\n if (resource && typeof resource === \"object\") {\n const meta = (resource as { meta?: { lastUpdated?: unknown } }).meta;\n const lu = meta?.lastUpdated;\n if (typeof lu === \"string\") {\n const parsed = new Date(lu);\n if (!Number.isNaN(parsed.getTime())) {\n return parsed;\n }\n }\n }\n if (\n typeof approximateCreationEpochMs === \"number\" &&\n Number.isFinite(approximateCreationEpochMs)\n ) {\n // The Kinesis Data Streams for DynamoDB pipe emits\n // `ApproximateCreationDateTime` in **milliseconds** — NOT seconds\n // (despite the DynamoDB Streams API of the same name being in\n // seconds). Pass the value directly to `Date`; multiplying by 1000\n // overshoots into the year-58000+ range.\n return new Date(approximateCreationEpochMs);\n }\n return new Date();\n}\n\ninterface UpsertParams {\n tenantId: string;\n workspaceId: string;\n resourceType: string;\n resourceId: string;\n version: string;\n lastUpdated: Date;\n resourceJson: string;\n}\n\ninterface SoftDeleteParams {\n tenantId: string;\n workspaceId: string;\n resourceType: string;\n resourceId: string;\n version: string;\n deletedAt: Date;\n}\n\ntype WriteIntent =\n | { kind: \"upsert\"; params: UpsertParams }\n | { kind: \"delete\"; params: SoftDeleteParams }\n | { kind: \"drop\" };\n\n/**\n * Translate a single Kinesis-wrapped DynamoDB stream record into a write\n * intent against the `resources` table. Returns `drop` for replica-side\n * global-table records, non-CURRENT items, and any record whose resource\n * payload cannot be decoded as JSON.\n */\nexport function buildWriteIntent(\n change: DynamoDbStreamKinesisRecord,\n awsRegion: string,\n): WriteIntent {\n if (shouldDropAsGlobalTableReplicationRecord(change, awsRegion)) {\n return { kind: \"drop\" };\n }\n const keys = parseCurrentResourceKeys(change);\n if (!keys) {\n return { kind: \"drop\" };\n }\n\n const isRemove = change.eventName === \"REMOVE\";\n const image = isRemove\n ? change.dynamodb?.OldImage\n : change.dynamodb?.NewImage;\n if (!image) {\n return { kind: \"drop\" };\n }\n\n const plain = dynamodbImageToPlain(image);\n const approxEpochMs = change.dynamodb?.ApproximateCreationDateTime;\n\n if (isRemove) {\n return {\n kind: \"delete\",\n params: {\n tenantId: keys.tenantId,\n workspaceId: keys.workspaceId,\n resourceType: keys.resourceType,\n resourceId: keys.resourceId,\n version: keys.version,\n deletedAt: deriveLastUpdated(undefined, approxEpochMs),\n },\n };\n }\n\n if (typeof plain.resource !== \"string\") {\n return { kind: \"drop\" };\n }\n // The DynamoDB `resource` attribute carries a gzip envelope\n // (`{ v, algo, payload }`) when the write went through the data-operations\n // layer. Decompress before parsing so the JSONB column holds the raw FHIR\n // resource — otherwise JSONB path queries (`resource->>'resourceType'`,\n // GIN-anchored search params) see envelope keys (`v`, `algo`, `payload`)\n // instead of FHIR fields. `decompressResource` accepts uncompressed JSON\n // as a pass-through, so this is safe for legacy/raw inputs too.\n let resourceJson: string;\n let resourceObj: unknown;\n try {\n resourceJson = decompressResource(plain.resource);\n resourceObj = JSON.parse(resourceJson);\n } catch {\n return { kind: \"drop\" };\n }\n\n return {\n kind: \"upsert\",\n params: {\n tenantId: keys.tenantId,\n workspaceId: keys.workspaceId,\n resourceType: keys.resourceType,\n resourceId: keys.resourceId,\n version: keys.version,\n lastUpdated: deriveLastUpdated(resourceObj, approxEpochMs),\n resourceJson,\n },\n };\n}\n\ninterface PgQueryable {\n query: (\n text: string,\n values?: ReadonlyArray<unknown>,\n ) => Promise<unknown> | unknown;\n}\n\n/**\n * Execute a write intent against an open Postgres connection or pool. Pure\n * SQL dispatch — extracted from the handler so unit tests can drive it with a\n * mock client and integration tests can drive it with a real pool.\n */\nexport async function applyWriteIntent(\n client: PgQueryable,\n schemaName: string,\n intent: WriteIntent,\n): Promise<void> {\n if (intent.kind === \"drop\") {\n return;\n }\n if (intent.kind === \"upsert\") {\n const p = intent.params;\n await Promise.resolve(\n client.query(buildResourceUpsertSql(schemaName), [\n p.tenantId,\n p.workspaceId,\n p.resourceType,\n p.resourceId,\n p.version,\n p.lastUpdated,\n p.resourceJson,\n ]),\n );\n return;\n }\n const p = intent.params;\n await Promise.resolve(\n client.query(buildResourceSoftDeleteSql(schemaName), [\n p.tenantId,\n p.workspaceId,\n p.resourceType,\n p.resourceId,\n p.version,\n p.deletedAt,\n ]),\n );\n}\n\nexport async function handler(\n event: KinesisStreamEvent,\n): Promise<KinesisStreamBatchResponse> {\n const awsRegion = process.env.AWS_REGION ?? \"\";\n const batchItemFailures: Array<{ itemIdentifier: string }> = [];\n\n await ensureBootstrapped();\n const cfg = await loadConfig();\n const p = await getPool();\n\n for (const record of event.Records) {\n try {\n const change = decodeKinesisRecord(record);\n const intent = buildWriteIntent(change, awsRegion);\n await applyWriteIntent(p, cfg.schema, intent);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error(\n \"postgres replication record failed\",\n record.kinesis.sequenceNumber,\n err,\n );\n batchItemFailures.push({\n itemIdentifier: record.kinesis.sequenceNumber,\n });\n }\n }\n\n return { batchItemFailures };\n}\n"],"mappings":"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","names":["p"]}
1
+ {"version":3,"sources":["../src/components/postgres/data-store-postgres-replication.handler.ts"],"sourcesContent":["import {\n GetSecretValueCommand,\n SecretsManagerClient,\n} from \"@aws-sdk/client-secrets-manager\";\nimport type {\n KinesisStreamBatchResponse,\n KinesisStreamEvent,\n KinesisStreamRecord,\n} from \"aws-lambda\";\nimport { Pool } from \"pg\";\nimport {\n buildResourceSoftDeleteSql,\n buildResourceUpsertSql,\n ensureSchemaBootstrap,\n} from \"./data-store-postgres-schema\";\nimport { decompressResource } from \"../../lib/compression\";\nimport {\n type DynamoDbStreamKinesisRecord,\n dynamodbImageToPlain,\n} from \"../dynamodb/dynamodb-stream-record\";\nimport {\n parseCurrentResourceKeys,\n shouldDropAsGlobalTableReplicationRecord,\n} from \"../dynamodb/firehose-archive-transform.handler\";\n\n/**\n * Postgres replication consumer for the data store change Kinesis stream\n * (ADR 2026-04-17-01, phase 1). Reads the same stream that feeds the\n * Firehose-to-S3 archive and projects each current FHIR resource into the\n * `resources` JSONB table on the Aurora Serverless v2 cluster provisioned by\n * {@link DataStorePostgresReplica}. This phase implements replication only;\n * no read-side query routing is wired up here.\n */\n\nconst REQUIRED_ENV_VARS = [\n \"OPENHI_PG_HOST\",\n \"OPENHI_PG_PORT\",\n \"OPENHI_PG_DATABASE\",\n \"OPENHI_PG_SCHEMA\",\n] as const;\n\ntype RequiredEnvVar = (typeof REQUIRED_ENV_VARS)[number];\n\ninterface PostgresConnectionConfig {\n host: string;\n port: number;\n database: string;\n schema: string;\n user: string;\n password: string;\n ssl: boolean;\n}\n\ninterface SecretsManagerCredentials {\n username: string;\n password: string;\n}\n\nlet cachedConfig: PostgresConnectionConfig | undefined;\nlet pool: Pool | undefined;\nlet bootstrapPromise: Promise<void> | undefined;\nlet secretsClient: SecretsManagerClient | undefined;\n\nfunction readEnv(name: RequiredEnvVar): string {\n const v = process.env[name]?.trim();\n if (!v) {\n throw new Error(\n `Missing required environment variable for postgres replication: ${name}`,\n );\n }\n return v;\n}\n\nasync function loadConfig(): Promise<PostgresConnectionConfig> {\n if (cachedConfig) {\n return cachedConfig;\n }\n const host = readEnv(\"OPENHI_PG_HOST\");\n const port = Number.parseInt(readEnv(\"OPENHI_PG_PORT\"), 10);\n const database = readEnv(\"OPENHI_PG_DATABASE\");\n const schema = readEnv(\"OPENHI_PG_SCHEMA\");\n\n // Test/integration setups can supply user/password directly via env to skip\n // the Secrets Manager round-trip. Production always goes through the secret.\n const directUser = process.env.OPENHI_PG_USER?.trim();\n const directPassword = process.env.OPENHI_PG_PASSWORD;\n if (directUser && directPassword !== undefined) {\n cachedConfig = {\n host,\n port,\n database,\n schema,\n user: directUser,\n password: directPassword,\n ssl: process.env.OPENHI_PG_SSL?.trim() === \"true\",\n };\n return cachedConfig;\n }\n\n const secretArn = process.env.OPENHI_PG_SECRET_ARN?.trim();\n if (!secretArn) {\n throw new Error(\n \"Either OPENHI_PG_SECRET_ARN or both OPENHI_PG_USER and OPENHI_PG_PASSWORD must be set\",\n );\n }\n if (!secretsClient) {\n secretsClient = new SecretsManagerClient({});\n }\n const out = await secretsClient.send(\n new GetSecretValueCommand({ SecretId: secretArn }),\n );\n if (!out.SecretString) {\n throw new Error(\n `Secret ${secretArn} returned no SecretString; binary secrets are not supported`,\n );\n }\n const parsed = JSON.parse(out.SecretString) as SecretsManagerCredentials;\n cachedConfig = {\n host,\n port,\n database,\n schema,\n user: parsed.username,\n password: parsed.password,\n ssl: true,\n };\n return cachedConfig;\n}\n\nasync function getPool(): Promise<Pool> {\n if (pool) {\n return pool;\n }\n const cfg = await loadConfig();\n pool = new Pool({\n host: cfg.host,\n port: cfg.port,\n database: cfg.database,\n user: cfg.user,\n password: cfg.password,\n ssl: cfg.ssl ? { rejectUnauthorized: false } : false,\n max: 2,\n idleTimeoutMillis: 60_000,\n connectionTimeoutMillis: 10_000,\n });\n return pool;\n}\n\nasync function ensureBootstrapped(): Promise<void> {\n if (!bootstrapPromise) {\n bootstrapPromise = (async () => {\n const cfg = await loadConfig();\n const p = await getPool();\n await ensureSchemaBootstrap(p, cfg.schema);\n })().catch((err) => {\n // Reset so a subsequent invocation retries the bootstrap on a fresh\n // container; otherwise a transient failure during cold start would\n // poison every record forever.\n bootstrapPromise = undefined;\n throw err;\n });\n }\n return bootstrapPromise;\n}\n\n/**\n * Reset module-scoped caches. Test-only helper; do not invoke from production\n * code paths. Exposed so unit and integration tests can run multiple\n * scenarios within a single Jest worker without leaking state across cases.\n */\nexport function __resetReplicationModuleStateForTests(): void {\n cachedConfig = undefined;\n pool = undefined;\n bootstrapPromise = undefined;\n secretsClient = undefined;\n}\n\nfunction decodeKinesisRecord(\n record: KinesisStreamRecord,\n): DynamoDbStreamKinesisRecord {\n const json = Buffer.from(record.kinesis.data, \"base64\").toString(\"utf8\");\n return JSON.parse(json) as DynamoDbStreamKinesisRecord;\n}\n\nfunction deriveLastUpdated(\n resource: unknown,\n approximateCreationEpochMs: number | undefined,\n): Date {\n if (resource && typeof resource === \"object\") {\n const meta = (resource as { meta?: { lastUpdated?: unknown } }).meta;\n const lu = meta?.lastUpdated;\n if (typeof lu === \"string\") {\n const parsed = new Date(lu);\n if (!Number.isNaN(parsed.getTime())) {\n return parsed;\n }\n }\n }\n if (\n typeof approximateCreationEpochMs === \"number\" &&\n Number.isFinite(approximateCreationEpochMs)\n ) {\n // The Kinesis Data Streams for DynamoDB pipe emits\n // `ApproximateCreationDateTime` in **milliseconds** — NOT seconds\n // (despite the DynamoDB Streams API of the same name being in\n // seconds). Pass the value directly to `Date`; multiplying by 1000\n // overshoots into the year-58000+ range.\n return new Date(approximateCreationEpochMs);\n }\n return new Date();\n}\n\ninterface UpsertParams {\n tenantId: string;\n workspaceId: string;\n resourceType: string;\n resourceId: string;\n version: string;\n lastUpdated: Date;\n resourceJson: string;\n}\n\ninterface SoftDeleteParams {\n tenantId: string;\n workspaceId: string;\n resourceType: string;\n resourceId: string;\n version: string;\n deletedAt: Date;\n}\n\ntype WriteIntent =\n | { kind: \"upsert\"; params: UpsertParams }\n | { kind: \"delete\"; params: SoftDeleteParams }\n | { kind: \"drop\" };\n\n/**\n * Translate a single Kinesis-wrapped DynamoDB stream record into a write\n * intent against the `resources` table. Returns `drop` for replica-side\n * global-table records, non-CURRENT items, and any record whose resource\n * payload cannot be decoded as JSON.\n */\nexport function buildWriteIntent(\n change: DynamoDbStreamKinesisRecord,\n awsRegion: string,\n): WriteIntent {\n if (shouldDropAsGlobalTableReplicationRecord(change, awsRegion)) {\n return { kind: \"drop\" };\n }\n const parsed = parseCurrentResourceKeys(change);\n if (!parsed) {\n return { kind: \"drop\" };\n }\n // Normalize tenant / workspace / resource ids to lowercase before writing\n // so the Postgres composite PK is internally consistent regardless of the\n // casing the upstream pipeline supplies. `resource_type` is intentionally\n // NOT lowered — it carries the canonical FHIR PascalCase\n // (`Patient`, `ExplanationOfBenefit`) the upstream parser recovers from\n // the resource JSON. The search / soft-delete WHERE clauses keep LOWER()\n // on both sides as a defense against any legacy rows that escaped this\n // normalizer.\n const keys = {\n ...parsed,\n tenantId: parsed.tenantId.toLowerCase(),\n workspaceId: parsed.workspaceId.toLowerCase(),\n resourceId: parsed.resourceId.toLowerCase(),\n };\n\n const isRemove = change.eventName === \"REMOVE\";\n const image = isRemove\n ? change.dynamodb?.OldImage\n : change.dynamodb?.NewImage;\n if (!image) {\n return { kind: \"drop\" };\n }\n\n const plain = dynamodbImageToPlain(image);\n const approxEpochMs = change.dynamodb?.ApproximateCreationDateTime;\n\n if (isRemove) {\n return {\n kind: \"delete\",\n params: {\n tenantId: keys.tenantId,\n workspaceId: keys.workspaceId,\n resourceType: keys.resourceType,\n resourceId: keys.resourceId,\n version: keys.version,\n deletedAt: deriveLastUpdated(undefined, approxEpochMs),\n },\n };\n }\n\n if (typeof plain.resource !== \"string\") {\n return { kind: \"drop\" };\n }\n // The DynamoDB `resource` attribute carries a gzip envelope\n // (`{ v, algo, payload }`) when the write went through the data-operations\n // layer. Decompress before parsing so the JSONB column holds the raw FHIR\n // resource — otherwise JSONB path queries (`resource->>'resourceType'`,\n // GIN-anchored search params) see envelope keys (`v`, `algo`, `payload`)\n // instead of FHIR fields. `decompressResource` accepts uncompressed JSON\n // as a pass-through, so this is safe for legacy/raw inputs too.\n let resourceJson: string;\n let resourceObj: unknown;\n try {\n resourceJson = decompressResource(plain.resource);\n resourceObj = JSON.parse(resourceJson);\n } catch {\n return { kind: \"drop\" };\n }\n\n return {\n kind: \"upsert\",\n params: {\n tenantId: keys.tenantId,\n workspaceId: keys.workspaceId,\n resourceType: keys.resourceType,\n resourceId: keys.resourceId,\n version: keys.version,\n lastUpdated: deriveLastUpdated(resourceObj, approxEpochMs),\n resourceJson,\n },\n };\n}\n\ninterface PgQueryable {\n query: (\n text: string,\n values?: ReadonlyArray<unknown>,\n ) => Promise<unknown> | unknown;\n}\n\n/**\n * Execute a write intent against an open Postgres connection or pool. Pure\n * SQL dispatch — extracted from the handler so unit tests can drive it with a\n * mock client and integration tests can drive it with a real pool.\n */\nexport async function applyWriteIntent(\n client: PgQueryable,\n schemaName: string,\n intent: WriteIntent,\n): Promise<void> {\n if (intent.kind === \"drop\") {\n return;\n }\n if (intent.kind === \"upsert\") {\n const p = intent.params;\n await Promise.resolve(\n client.query(buildResourceUpsertSql(schemaName), [\n p.tenantId,\n p.workspaceId,\n p.resourceType,\n p.resourceId,\n p.version,\n p.lastUpdated,\n p.resourceJson,\n ]),\n );\n return;\n }\n const p = intent.params;\n await Promise.resolve(\n client.query(buildResourceSoftDeleteSql(schemaName), [\n p.tenantId,\n p.workspaceId,\n p.resourceType,\n p.resourceId,\n p.version,\n p.deletedAt,\n ]),\n );\n}\n\nexport async function handler(\n event: KinesisStreamEvent,\n): Promise<KinesisStreamBatchResponse> {\n const awsRegion = process.env.AWS_REGION ?? \"\";\n const batchItemFailures: Array<{ itemIdentifier: string }> = [];\n\n await ensureBootstrapped();\n const cfg = await loadConfig();\n const p = await getPool();\n\n for (const record of event.Records) {\n try {\n const change = decodeKinesisRecord(record);\n const intent = buildWriteIntent(change, awsRegion);\n await applyWriteIntent(p, cfg.schema, intent);\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error(\n \"postgres replication record failed\",\n record.kinesis.sequenceNumber,\n err,\n );\n batchItemFailures.push({\n itemIdentifier: record.kinesis.sequenceNumber,\n });\n }\n }\n\n return { batchItemFailures };\n}\n"],"mappings":"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","names":["p"]}
@@ -10964,6 +10964,16 @@ function jsonbPathToStringShape(jsonbPath) {
10964
10964
  if (arrayOfScalars) {
10965
10965
  return { kind: "array-of-scalars", field: arrayOfScalars[1] };
10966
10966
  }
10967
+ const arrayOfArrays = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]$/.exec(
10968
+ jsonbPath
10969
+ );
10970
+ if (arrayOfArrays) {
10971
+ return {
10972
+ kind: "array-of-arrays",
10973
+ field: arrayOfArrays[1],
10974
+ subfield: arrayOfArrays[2]
10975
+ };
10976
+ }
10967
10977
  const arrayOfObjs = /^\$\.([A-Za-z_][A-Za-z0-9_]*)\[\*\]\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(
10968
10978
  jsonbPath
10969
10979
  );
@@ -10975,7 +10985,7 @@ function jsonbPathToStringShape(jsonbPath) {
10975
10985
  };
10976
10986
  }
10977
10987
  throw new Error(
10978
- `String predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield".`
10988
+ `String predicate cannot translate JSONPath "${jsonbPath}". Supported shapes: "$.field", "$.field[*]", "$.field[*].subfield", "$.field[*].subfield[*]".`
10979
10989
  );
10980
10990
  }
10981
10991
  function escapeLikePattern(value) {
@@ -11008,6 +11018,13 @@ function buildIlikeExtractSql(shape, paramName) {
11008
11018
  `jsonb_array_elements(resource->'${shape.field}') AS s_obj(obj)`,
11009
11019
  `WHERE s_obj.obj->>'${shape.subfield}' ILIKE :${paramName})`
11010
11020
  ].join(" ");
11021
+ case "array-of-arrays":
11022
+ return [
11023
+ "EXISTS (SELECT 1 FROM",
11024
+ `jsonb_array_elements(resource->'${shape.field}') AS s_obj(obj),`,
11025
+ `jsonb_array_elements_text(s_obj.obj->'${shape.subfield}') AS s_elem(text_val)`,
11026
+ `WHERE s_elem.text_val ILIKE :${paramName})`
11027
+ ].join(" ");
11011
11028
  }
11012
11029
  }
11013
11030
  function emitStringPredicate(opts) {
@@ -11248,8 +11265,8 @@ function buildGenericSearchSql(opts) {
11248
11265
  const lines = [
11249
11266
  "SELECT resource_id AS id, resource",
11250
11267
  "FROM resources",
11251
- "WHERE tenant_id = :tenantId",
11252
- " AND workspace_id = :workspaceId",
11268
+ "WHERE LOWER(tenant_id) = LOWER(:tenantId)",
11269
+ " AND LOWER(workspace_id) = LOWER(:workspaceId)",
11253
11270
  " AND LOWER(resource_type) = LOWER(:resourceType)",
11254
11271
  " AND deleted_at IS NULL"
11255
11272
  ];
@@ -11280,12 +11297,23 @@ async function genericSearchOperation(params) {
11280
11297
  ...combined.params
11281
11298
  ];
11282
11299
  const rows = await runner.query(sql, queryParams);
11283
- const entries = rows.map((row) => ({
11284
- id: row.id,
11285
- resource: { ...row.resource, id: row.id }
11286
- }));
11300
+ const entries = rows.map((row) => {
11301
+ const parsed = parseResourceColumn(row.resource);
11302
+ const bodyId = typeof parsed.id === "string" ? parsed.id : void 0;
11303
+ const id = bodyId ?? row.id;
11304
+ return {
11305
+ id,
11306
+ resource: { ...parsed, id }
11307
+ };
11308
+ });
11287
11309
  return { entries, total: entries.length };
11288
11310
  }
11311
+ function parseResourceColumn(raw) {
11312
+ if (typeof raw === "string") {
11313
+ return JSON.parse(raw);
11314
+ }
11315
+ return raw;
11316
+ }
11289
11317
 
11290
11318
  // src/data/search/registry/allergyintolerance-search-parameters.ts
11291
11319
  var ALLERGYINTOLERANCE_SEARCH_PARAMETERS = [
@@ -11886,7 +11914,7 @@ var PATIENT_SEARCH_PARAMETERS = [
11886
11914
  {
11887
11915
  code: "given",
11888
11916
  type: "string",
11889
- jsonbPath: "$.name[*].given",
11917
+ jsonbPath: "$.name[*].given[*]",
11890
11918
  modifiers: ["exact", "contains", "missing", "not"]
11891
11919
  },
11892
11920
  { code: "active", type: "token", jsonbPath: "$.active" },
@@ -11936,7 +11964,7 @@ var PRACTITIONER_SEARCH_PARAMETERS = [
11936
11964
  {
11937
11965
  code: "given",
11938
11966
  type: "string",
11939
- jsonbPath: "$.name[*].given",
11967
+ jsonbPath: "$.name[*].given[*]",
11940
11968
  modifiers: ["exact", "contains", "missing", "not"]
11941
11969
  },
11942
11970
  { code: "active", type: "token", jsonbPath: "$.active" },