@openhi/constructs 0.0.117 → 0.0.119
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/lib/{chunk-QWWLM452.mjs → chunk-7WDX6GPO.mjs} +57 -1
- package/lib/chunk-7WDX6GPO.mjs.map +1 -0
- package/lib/{chunk-AJQUWHFK.mjs → chunk-WXS3PUHR.mjs} +2 -2
- package/lib/index.d.mts +56 -15
- package/lib/index.d.ts +56 -15
- package/lib/index.js +77 -39
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +79 -41
- package/lib/index.mjs.map +1 -1
- package/lib/provision-default-workspace.handler.js +53 -0
- package/lib/provision-default-workspace.handler.js.map +1 -1
- package/lib/provision-default-workspace.handler.mjs +1 -1
- package/lib/rest-api-lambda.handler.js +85 -0
- package/lib/rest-api-lambda.handler.js.map +1 -1
- package/lib/rest-api-lambda.handler.mjs +34 -1
- package/lib/rest-api-lambda.handler.mjs.map +1 -1
- package/lib/seed-demo-data.handler.js +53 -0
- package/lib/seed-demo-data.handler.js.map +1 -1
- package/lib/seed-demo-data.handler.mjs +2 -2
- package/package.json +3 -3
- package/lib/chunk-QWWLM452.mjs.map +0 -1
- /package/lib/{chunk-AJQUWHFK.mjs.map → chunk-WXS3PUHR.mjs.map} +0 -0
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getDynamoDataService
|
|
16
16
|
} from "./chunk-U7L7T4XU.mjs";
|
|
17
17
|
import {
|
|
18
|
+
ConflictError,
|
|
18
19
|
ValidationError
|
|
19
20
|
} from "./chunk-FYHBHHWK.mjs";
|
|
20
21
|
import {
|
|
@@ -42,6 +43,19 @@ function extractDenormalizedReferenceDisplay(resource, fieldName) {
|
|
|
42
43
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
// src/data/operations/control/membership-constraints/assert-workspace-in-tenant-operation.ts
|
|
47
|
+
async function assertWorkspaceInTenantOperation(params) {
|
|
48
|
+
const { tenantId, workspaceId, tableName } = params;
|
|
49
|
+
const service = getDynamoControlService(tableName);
|
|
50
|
+
const { data: item } = await service.entities.workspace.get({ tenantId, id: workspaceId, sk: "CURRENT" }).go();
|
|
51
|
+
if (!item) {
|
|
52
|
+
throw new ConflictError(
|
|
53
|
+
`Workspace ${workspaceId} does not belong to tenant ${tenantId}; the workspace must be created in the referenced tenant before this resource can reference it.`,
|
|
54
|
+
{ details: { tenantId, workspaceId } }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
// src/data/operations/control/membership/membership-create-operation.ts
|
|
46
60
|
async function createMembershipOperation(params) {
|
|
47
61
|
const { context, body, tableName } = params;
|
|
@@ -82,6 +96,15 @@ async function createMembershipOperation(params) {
|
|
|
82
96
|
resourceRecord,
|
|
83
97
|
"workspace"
|
|
84
98
|
);
|
|
99
|
+
if (workspaceIdFromResource !== void 0) {
|
|
100
|
+
const tenantIdFromResource = extractReferenceSlug(resourceRecord, "tenant");
|
|
101
|
+
const referencedTenantId = tenantIdFromResource ?? context.tenantId;
|
|
102
|
+
await assertWorkspaceInTenantOperation({
|
|
103
|
+
tenantId: referencedTenantId,
|
|
104
|
+
workspaceId: workspaceIdFromResource,
|
|
105
|
+
tableName
|
|
106
|
+
});
|
|
107
|
+
}
|
|
85
108
|
const userProjectionItem = userIdFromResource !== void 0 ? buildMembershipUserProjectionItem({
|
|
86
109
|
tenantId: context.tenantId,
|
|
87
110
|
userId: userIdFromResource,
|
|
@@ -142,6 +165,23 @@ async function createMembershipOperation(params) {
|
|
|
142
165
|
|
|
143
166
|
// src/data/operations/control/roleassignment/roleassignment-create-operation.ts
|
|
144
167
|
import { extractSummary as extractSummary2 } from "@openhi/types";
|
|
168
|
+
|
|
169
|
+
// src/data/operations/control/membership-constraints/assert-user-has-tenant-membership-operation.ts
|
|
170
|
+
var TENANT_LANE_SK_PREFIX = "MEMBERSHIP#TENANT#";
|
|
171
|
+
async function assertUserHasTenantMembershipOperation(params) {
|
|
172
|
+
const { userId, tenantId, tableName } = params;
|
|
173
|
+
const service = getDynamoControlService(tableName);
|
|
174
|
+
const result = await service.entities.membershipUserProjection.query.record({ userId }).begins({ sk: TENANT_LANE_SK_PREFIX }).go();
|
|
175
|
+
const matched = (result.data ?? []).some((row) => row.tenantId === tenantId);
|
|
176
|
+
if (!matched) {
|
|
177
|
+
throw new ConflictError(
|
|
178
|
+
`User ${userId} has no tenant-level Membership in tenant ${tenantId}; a Membership must exist before a RoleAssignment can be created.`,
|
|
179
|
+
{ details: { userId, tenantId } }
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/data/operations/control/roleassignment/roleassignment-create-operation.ts
|
|
145
185
|
async function createRoleAssignmentOperation(params) {
|
|
146
186
|
const { context, body, tableName } = params;
|
|
147
187
|
const service = getDynamoControlService(tableName);
|
|
@@ -170,6 +210,22 @@ async function createRoleAssignmentOperation(params) {
|
|
|
170
210
|
resourceRecord,
|
|
171
211
|
"workspace"
|
|
172
212
|
);
|
|
213
|
+
if (userIdFromResource !== void 0) {
|
|
214
|
+
const tenantIdFromResource = extractReferenceSlug2(resourceRecord, "tenant");
|
|
215
|
+
const referencedTenantId = tenantIdFromResource ?? context.tenantId;
|
|
216
|
+
await assertUserHasTenantMembershipOperation({
|
|
217
|
+
userId: userIdFromResource,
|
|
218
|
+
tenantId: referencedTenantId,
|
|
219
|
+
tableName
|
|
220
|
+
});
|
|
221
|
+
if (workspaceIdFromResource !== void 0) {
|
|
222
|
+
await assertWorkspaceInTenantOperation({
|
|
223
|
+
tenantId: referencedTenantId,
|
|
224
|
+
workspaceId: workspaceIdFromResource,
|
|
225
|
+
tableName
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
173
229
|
const userProjectionItem = userIdFromResource !== void 0 && roleIdFromResource !== void 0 ? buildRoleAssignmentUserProjectionItem({
|
|
174
230
|
tenantId: context.tenantId,
|
|
175
231
|
userId: userIdFromResource,
|
|
@@ -330,4 +386,4 @@ export {
|
|
|
330
386
|
createTenantOperation,
|
|
331
387
|
createWorkspaceOperation
|
|
332
388
|
};
|
|
333
|
-
//# sourceMappingURL=chunk-
|
|
389
|
+
//# sourceMappingURL=chunk-7WDX6GPO.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/data/operations/control/membership/membership-create-operation.ts","../src/data/operations/control/denormalized-display-names.ts","../src/data/operations/control/membership-constraints/assert-workspace-in-tenant-operation.ts","../src/data/operations/control/roleassignment/roleassignment-create-operation.ts","../src/data/operations/control/membership-constraints/assert-user-has-tenant-membership-operation.ts","../src/data/operations/control/tenant/tenant-create-operation.ts","../src/data/operations/control/workspace/workspace-create-operation.ts","../src/data/operations/data/organization/organization-provision-for-workspace-operation.ts"],"sourcesContent":["import {\n assertLinkedDataIdentityCardinality,\n extractSummary,\n type Extension,\n type FhirResourceLike,\n LinkedDataIdentityCardinalityError,\n} from \"@openhi/types\";\nimport {\n buildMembershipUserProjectionItem,\n extractReferenceSlug,\n} from \"./membership-user-projection\";\nimport { buildMembershipWorkspaceProjectionItem } from \"./membership-workspace-projection\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { ValidationError } from \"../../../errors\";\nimport { OpenHiContext } from \"../../../openhi-context\";\nimport { extractDenormalizedReferenceDisplay } from \"../denormalized-display-names\";\nimport { assertWorkspaceInTenantOperation } from \"../membership-constraints/assert-workspace-in-tenant-operation\";\nimport {\n executeMultiWrite,\n type MultiWriteTriple,\n} from \"../multi-write-operation\";\n\nexport interface MembershipCreateParams {\n context: OpenHiContext;\n body: { id?: string; resource?: Record<string, unknown> | string };\n tableName?: string;\n}\n\nexport interface MembershipCreateResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n meta: { lastUpdated: string; versionId: string };\n}\n\nexport async function createMembershipOperation(\n params: MembershipCreateParams,\n): Promise<MembershipCreateResult> {\n const { context, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `membership-${Date.now()}`;\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const lastUpdated = context.date ?? new Date().toISOString();\n const vid = `1`;\n\n const resource = { resourceType: \"Membership\", id, ...parsedResource };\n\n let linkedDataIdentityRef: string | undefined;\n try {\n const ext = assertLinkedDataIdentityCardinality(\n resource as { extension?: Array<Extension> },\n );\n linkedDataIdentityRef = ext?.valueReference?.reference;\n } catch (e) {\n if (e instanceof LinkedDataIdentityCardinalityError) {\n throw new ValidationError(e.message, { cause: e });\n }\n throw e;\n }\n\n // TR-024 denormalized display-name attributes. The authoritative\n // write-time source per TR-024 rule 2 is the canonical Tenant / User\n // record's `displayName`. Until the carrier-record-lookup pass lands\n // (#1010 follow-up), the foundational fallback (#1009) reads the\n // FHIR Reference.display values supplied on the resource so the\n // adjacency-list projection writer here can compose SKs from\n // top-level attributes.\n const resourceRecord = resource as Record<string, unknown>;\n const denormalizedTenantName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"tenant\",\n );\n const denormalizedUserName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"user\",\n );\n const denormalizedWorkspaceName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"workspace\",\n );\n\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n // ADR-018 patterns #2 / #3 / #4 — user- and workspace-partition\n // projection rows. The projection's discriminating fields come from\n // the same FHIR Reference shape the canonical row reads, so the\n // canonical and projection writes always agree on the underlying\n // user / workspace identifiers. Missing identifiers (no user\n // reference at all) skip the user-projection; tenant-scoped\n // Memberships (no workspaceId) skip the workspace-projection — the\n // canonical row still lands either way.\n const userIdFromResource = extractReferenceSlug(resourceRecord, \"user\");\n const workspaceIdFromResource = extractReferenceSlug(\n resourceRecord,\n \"workspace\",\n );\n\n // ADR 2026-03-13-02 § 2 Consequences/Negative — when the resource\n // names a workspace, that workspace must belong to the referenced\n // tenant. Prefer the resource's `tenant.reference` (the issue's\n // \"referenced tenant\" wording) and fall back to the context tenant\n // when the body omits the reference. The check fires before any\n // write so the constraint is enforced atomically with respect to\n // the canonical row.\n if (workspaceIdFromResource !== undefined) {\n const tenantIdFromResource = extractReferenceSlug(resourceRecord, \"tenant\");\n const referencedTenantId = tenantIdFromResource ?? context.tenantId;\n await assertWorkspaceInTenantOperation({\n tenantId: referencedTenantId,\n workspaceId: workspaceIdFromResource,\n tableName,\n });\n }\n\n const userProjectionItem =\n userIdFromResource !== undefined\n ? buildMembershipUserProjectionItem({\n tenantId: context.tenantId,\n userId: userIdFromResource,\n workspaceId: workspaceIdFromResource,\n membershipId: id,\n summary,\n vid,\n lastUpdated,\n denormalizedTenantName,\n denormalizedUserName,\n denormalizedWorkspaceName,\n })\n : undefined;\n\n const workspaceProjectionItem =\n userIdFromResource !== undefined && workspaceIdFromResource !== undefined\n ? buildMembershipWorkspaceProjectionItem({\n tenantId: context.tenantId,\n workspaceId: workspaceIdFromResource,\n userId: userIdFromResource,\n membershipId: id,\n summary,\n vid,\n lastUpdated,\n denormalizedUserName,\n })\n : undefined;\n\n const canonicalItem = {\n tenantId: context.tenantId,\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n linkedDataIdentityRef,\n denormalizedTenantName,\n denormalizedUserName,\n };\n\n const triples: Array<MultiWriteTriple> = [\n { entity: \"membership\", action: \"put\", item: canonicalItem },\n ];\n if (userProjectionItem) {\n triples.push({\n entity: \"membershipUserProjection\",\n action: \"put\",\n item: userProjectionItem as unknown as Record<string, unknown>,\n });\n }\n if (workspaceProjectionItem) {\n triples.push({\n entity: \"membershipWorkspaceProjection\",\n action: \"put\",\n item: workspaceProjectionItem as unknown as Record<string, unknown>,\n });\n }\n\n await executeMultiWrite({ service, triples });\n\n return {\n id,\n resource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","/**\n * Helpers that capture denormalized display-name attributes from an\n * incoming FHIR Reference on Membership and RoleAssignment resources.\n *\n * **Foundational implementation for TR-024.** ADR-018 § Implementation\n * Notes and TR-024 § Recommendation pin the authoritative write-time\n * source as the carrier entity's canonical `displayName` — i.e. the\n * operations layer reads the canonical Tenant / User / Role record\n * by id and copies its display name into the relationship's\n * `denormalized<CarrierEntity>Name` field on the same\n * `TransactWriteItems`. That contract is owned by the operations-layer\n * multi-write helper filed as #1010.\n *\n * This module is the **foundational fallback** that issue #1009 uses\n * before #1010 lands: it reads the display string a client already\n * supplies on the resource's `Reference.display` field. The fallback\n * keeps Membership / RoleAssignment write paths populating the new\n * top-level attributes today (so adjacency-list projection writers\n * downstream can rely on them) while letting #1010 own the canonical-\n * row lookup without re-plumbing the write paths.\n *\n * @see TR-024 — Denormalized display-name attributes\n * @see ADR-018 § Implementation Notes\n */\n\n/**\n * Returns the trimmed display string from `resource[fieldName].display`\n * when present and non-empty; otherwise returns `undefined`. Used by the\n * Membership and RoleAssignment create / update operations to populate\n * top-level `denormalized<CarrierEntity>Name` attributes from incoming\n * FHIR Reference fields.\n *\n * Field name maps to the FHIR field on the resource:\n * - `Membership.tenant` → `\"tenant\"`\n * - `Membership.user` → `\"user\"`\n * - `RoleAssignment.tenant` → `\"tenant\"`\n * - `RoleAssignment.user` → `\"user\"`\n * - `RoleAssignment.role` → `\"role\"`\n *\n * Guards against malformed payloads (non-object `field`, non-string\n * `display`, empty strings after trim) so a single bad write never\n * blocks an entity put — matching the same defensive posture\n * `gsi1skAttribute` takes for the `resource` JSON parse.\n */\nexport function extractDenormalizedReferenceDisplay(\n resource: Record<string, unknown>,\n fieldName: string,\n): string | undefined {\n const field = resource[fieldName];\n if (!field || typeof field !== \"object\") {\n return undefined;\n }\n const display = (field as { display?: unknown }).display;\n if (typeof display !== \"string\") {\n return undefined;\n }\n const trimmed = display.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { ConflictError } from \"../../../errors\";\n\n/** Inputs to {@link assertWorkspaceInTenantOperation}. */\nexport interface AssertWorkspaceInTenantParams {\n /** Tenant the workspace must belong to. */\n readonly tenantId: string;\n /** Workspace id that must exist under `tenantId`. */\n readonly workspaceId: string;\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/**\n * Throws {@link ConflictError} when no `Workspace` with id `workspaceId`\n * exists under `tenantId`.\n *\n * Implementation: a single base-table `get` against the Workspace entity\n * (`PK = TID#<tenantId>#WORKSPACE#ID#<workspaceId>, SK = CURRENT`). The\n * workspace's tenant is encoded into the PK by construction, so a hit\n * *is* the constraint check — there is no separate `managingOrganization`\n * comparison required.\n *\n * @see ADR 2026-03-13-02 § 2 — Consequences/Negative\n */\nexport async function assertWorkspaceInTenantOperation(\n params: AssertWorkspaceInTenantParams,\n): Promise<void> {\n const { tenantId, workspaceId, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const { data: item } = await service.entities.workspace\n .get({ tenantId, id: workspaceId, sk: \"CURRENT\" })\n .go();\n\n if (!item) {\n throw new ConflictError(\n `Workspace ${workspaceId} does not belong to tenant ${tenantId}; the workspace must be created in the referenced tenant before this resource can reference it.`,\n { details: { tenantId, workspaceId } },\n );\n }\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport {\n buildRoleAssignmentUserProjectionItem,\n extractReferenceSlug,\n} from \"./roleassignment-user-projection\";\nimport { buildRoleAssignmentWorkspaceProjectionItem } from \"./roleassignment-workspace-projection\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { OpenHiContext } from \"../../../openhi-context\";\nimport { extractDenormalizedReferenceDisplay } from \"../denormalized-display-names\";\nimport { assertUserHasTenantMembershipOperation } from \"../membership-constraints/assert-user-has-tenant-membership-operation\";\nimport { assertWorkspaceInTenantOperation } from \"../membership-constraints/assert-workspace-in-tenant-operation\";\nimport {\n executeMultiWrite,\n type MultiWriteTriple,\n} from \"../multi-write-operation\";\n\nexport interface RoleAssignmentCreateParams {\n context: OpenHiContext;\n body: { id?: string; resource?: Record<string, unknown> | string };\n tableName?: string;\n}\n\nexport interface RoleAssignmentCreateResult {\n id: string;\n resource: { resourceType: string; id: string; [key: string]: unknown };\n meta: { lastUpdated: string; versionId: string };\n}\n\nexport async function createRoleAssignmentOperation(\n params: RoleAssignmentCreateParams,\n): Promise<RoleAssignmentCreateResult> {\n const { context, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `roleassignment-${Date.now()}`;\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const lastUpdated = context.date ?? new Date().toISOString();\n const vid = `1`;\n\n const resource = { resourceType: \"RoleAssignment\", id, ...parsedResource };\n\n // TR-024 denormalized display-name attributes. The authoritative\n // write-time source per TR-024 rule 2 is the canonical Tenant / User /\n // Role record's `displayName`. Until the carrier-record-lookup pass\n // lands (#1010 follow-up), the foundational fallback (#1009) reads\n // the FHIR Reference.display values supplied on the resource so the\n // adjacency-list projection writer here can compose SKs from\n // top-level attributes.\n const resourceRecord = resource as Record<string, unknown>;\n const denormalizedTenantName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"tenant\",\n );\n const denormalizedUserName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"user\",\n );\n const denormalizedRoleName = extractDenormalizedReferenceDisplay(\n resourceRecord,\n \"role\",\n );\n\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n // ADR-018 patterns #5 / #9 — user- and workspace-partition projection\n // rows. The projection's discriminating fields come from the same\n // FHIR Reference shape the canonical row reads, so the canonical and\n // projection writes always agree on the underlying user / role /\n // workspace identifiers. Missing user or role references skip the\n // user-projection; tenant-scoped RoleAssignments (no workspaceId)\n // skip the workspace-projection — the canonical row still lands\n // either way.\n const userIdFromResource = extractReferenceSlug(resourceRecord, \"user\");\n const roleIdFromResource = extractReferenceSlug(resourceRecord, \"role\");\n const workspaceIdFromResource = extractReferenceSlug(\n resourceRecord,\n \"workspace\",\n );\n\n // ADR 2026-03-13-02 § 2 Consequences/Negative — a RoleAssignment may\n // only be created when the referenced user already holds a\n // tenant-level Membership in the referenced tenant, and (when\n // workspace-scoped) that workspace must belong to the same tenant.\n // Prefer the resource's `tenant.reference` (the issue's \"referenced\n // tenant\" wording) and fall back to the context tenant when the\n // body omits the reference. Constraints fire before any write so\n // the rejection is atomic with respect to the canonical row.\n if (userIdFromResource !== undefined) {\n const tenantIdFromResource = extractReferenceSlug(resourceRecord, \"tenant\");\n const referencedTenantId = tenantIdFromResource ?? context.tenantId;\n await assertUserHasTenantMembershipOperation({\n userId: userIdFromResource,\n tenantId: referencedTenantId,\n tableName,\n });\n if (workspaceIdFromResource !== undefined) {\n await assertWorkspaceInTenantOperation({\n tenantId: referencedTenantId,\n workspaceId: workspaceIdFromResource,\n tableName,\n });\n }\n }\n\n const userProjectionItem =\n userIdFromResource !== undefined && roleIdFromResource !== undefined\n ? buildRoleAssignmentUserProjectionItem({\n tenantId: context.tenantId,\n userId: userIdFromResource,\n workspaceId: workspaceIdFromResource,\n roleId: roleIdFromResource,\n roleAssignmentId: id,\n summary,\n vid,\n lastUpdated,\n denormalizedTenantName,\n denormalizedUserName,\n denormalizedRoleName,\n })\n : undefined;\n\n const workspaceProjectionItem =\n userIdFromResource !== undefined &&\n roleIdFromResource !== undefined &&\n workspaceIdFromResource !== undefined\n ? buildRoleAssignmentWorkspaceProjectionItem({\n tenantId: context.tenantId,\n workspaceId: workspaceIdFromResource,\n userId: userIdFromResource,\n roleId: roleIdFromResource,\n roleAssignmentId: id,\n summary,\n vid,\n lastUpdated,\n denormalizedUserName,\n denormalizedRoleName,\n })\n : undefined;\n\n const canonicalItem = {\n tenantId: context.tenantId,\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n denormalizedTenantName,\n denormalizedUserName,\n denormalizedRoleName,\n };\n\n const triples: Array<MultiWriteTriple> = [\n { entity: \"roleAssignment\", action: \"put\", item: canonicalItem },\n ];\n if (userProjectionItem) {\n triples.push({\n entity: \"roleAssignmentUserProjection\",\n action: \"put\",\n item: userProjectionItem as unknown as Record<string, unknown>,\n });\n }\n if (workspaceProjectionItem) {\n triples.push({\n entity: \"roleAssignmentWorkspaceProjection\",\n action: \"put\",\n item: workspaceProjectionItem as unknown as Record<string, unknown>,\n });\n }\n\n await executeMultiWrite({ service, triples });\n\n return {\n id,\n resource,\n meta: { lastUpdated, versionId: vid },\n };\n}\n","import { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport { ConflictError } from \"../../../errors\";\n\n/**\n * SK prefix for the tenant-level Membership sub-lane (ADR-018 pattern #3).\n *\n * Tenant-level Memberships project under the user partition with\n * `SK = MEMBERSHIP#TENANT#<normalizedTenantName>#TID#<tenantId>#<membershipId>`.\n * Because the `<tenantId>` segment appears *after* `<normalizedTenantName>`,\n * a `begins_with` filter cannot narrow to a single tenant — we read every\n * tenant-lane row for the user and filter on the projection-row's\n * `tenantId` attribute in memory.\n */\nconst TENANT_LANE_SK_PREFIX = \"MEMBERSHIP#TENANT#\";\n\n/** Inputs to {@link assertUserHasTenantMembershipOperation}. */\nexport interface AssertUserHasTenantMembershipParams {\n /** The user that must already be a member of `tenantId`. */\n readonly userId: string;\n /** The tenant the user must hold a tenant-level Membership in. */\n readonly tenantId: string;\n /** Optional table-name override; resolved via env when omitted. */\n readonly tableName?: string;\n}\n\n/**\n * Throws {@link ConflictError} when `userId` has no tenant-level\n * `Membership` in `tenantId`.\n *\n * Implementation: queries the ADR-018 user-partition projection\n * (`MembershipUserProjectionEntity`) under\n * `PK = USER#ID#<userId>, SK begins_with 'MEMBERSHIP#TENANT#'` — a single\n * strongly-consistent base-table query with no GSI hop — and filters the\n * returned rows for `tenantId` in memory.\n *\n * The filter cannot be expressed as an additional `begins_with` because\n * the projection SK encodes `<normalizedTenantName>` *before* `<tenantId>`\n * (`MEMBERSHIP#TENANT#<normalizedTenantName>#TID#<tenantId>#<id>`). The\n * scan cost is bounded by the user's tenant-membership fan-out, which is\n * small by construction.\n *\n * @see ADR 2026-03-13-02 § 2 — Consequences/Negative\n * @see ADR-018 § Access Pattern Coverage (pattern #3)\n */\nexport async function assertUserHasTenantMembershipOperation(\n params: AssertUserHasTenantMembershipParams,\n): Promise<void> {\n const { userId, tenantId, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const result = await service.entities.membershipUserProjection.query\n .record({ userId })\n .begins({ sk: TENANT_LANE_SK_PREFIX })\n .go();\n\n const matched = (result.data ?? []).some((row) => row.tenantId === tenantId);\n if (!matched) {\n throw new ConflictError(\n `User ${userId} has no tenant-level Membership in tenant ${tenantId}; a Membership must exist before a RoleAssignment can be created.`,\n { details: { userId, tenantId } },\n );\n }\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\n\nexport interface CreateTenantParams {\n context: OpenHiContext;\n body: {\n id?: string;\n resource?: Record<string, unknown> | string;\n };\n tableName?: string;\n}\n\nexport interface CreateTenantResult {\n id: string;\n resource: Record<string, unknown>;\n meta: { lastUpdated: string; versionId: string };\n}\n\n/**\n * Creates a Tenant. Generates an id if not provided.\n */\nexport async function createTenantOperation(\n params: CreateTenantParams,\n): Promise<CreateTenantResult> {\n const { context, body, tableName } = params;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `tenant-${Date.now()}`;\n const lastUpdated = context.date;\n const vid =\n lastUpdated.replace(/[-:T.Z]/g, \"\").slice(0, 12) || Date.now().toString(36);\n\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const resource = { resourceType: \"Tenant\", id, ...parsedResource };\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n await service.entities.tenant\n .put({\n tenantId: id,\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n return { id, resource, meta: { lastUpdated, versionId: vid } };\n}\n","import { extractSummary, type FhirResourceLike } from \"@openhi/types\";\nimport { getDynamoControlService } from \"../../../dynamo/dynamo-control-service\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\nimport { provisionOrganizationForWorkspaceOperation } from \"../../data/organization/organization-provision-for-workspace-operation\";\n\nexport interface CreateWorkspaceParams {\n context: OpenHiContext;\n body: {\n id?: string;\n resource?: Record<string, unknown> | string;\n };\n tableName?: string;\n}\n\nexport interface CreateWorkspaceResult {\n id: string;\n resource: Record<string, unknown>;\n meta: { lastUpdated: string; versionId: string };\n}\n\n/**\n * Creates a Workspace scoped to the context tenant. Generates an id if not provided.\n */\nexport async function createWorkspaceOperation(\n params: CreateWorkspaceParams,\n): Promise<CreateWorkspaceResult> {\n const { context, body, tableName } = params;\n const { tenantId } = context;\n const service = getDynamoControlService(tableName);\n\n const id = body.id ?? `workspace-${Date.now()}`;\n const lastUpdated = context.date;\n const vid =\n lastUpdated.replace(/[-:T.Z]/g, \"\").slice(0, 12) || Date.now().toString(36);\n\n const parsedResource =\n typeof body.resource === \"string\"\n ? (JSON.parse(body.resource) as Record<string, unknown>)\n : (body.resource ?? {});\n\n const resource = { resourceType: \"Workspace\", id, ...parsedResource };\n const summary = JSON.stringify(extractSummary(resource as FhirResourceLike));\n\n await service.entities.workspace\n .put({\n tenantId,\n id,\n resource: JSON.stringify(resource),\n summary,\n vid,\n lastUpdated,\n })\n .go();\n\n const workspaceName =\n typeof parsedResource.name === \"string\" ? parsedResource.name : undefined;\n await provisionOrganizationForWorkspaceOperation({\n context,\n workspaceId: id,\n workspaceName,\n tableName,\n });\n\n return { id, resource, meta: { lastUpdated, versionId: vid } };\n}\n","import type { Organization, Reference } from \"@openhi/types\";\nimport { getDynamoDataService } from \"../../../dynamo/dynamo-data-service\";\nimport type { OpenHiContext } from \"../../../openhi-context\";\nimport {\n createDataEntityRecord,\n DATA_ENTITY_SK,\n type SingleResourceResult,\n} from \"../../data-operations-common\";\n\n/**\n * Provision the slim, id-share Organization that represents a Workspace on the data plane.\n *\n * - `Organization.id === workspaceId` (id-share with the Workspace).\n * - `name` is populated from the Workspace's name when provided.\n * - `partOf` is populated when the Workspace's Tenant has an Organization (id-share at the\n * tenant level: `Tenant.id === TenantOrganization.id`). Tenant-level provisioning is out of\n * scope for #1001 — the lookup returns `undefined` until Tenant Organizations exist.\n *\n * Idempotent: re-running with the same params overwrites the same PK with the same payload.\n *\n * @see https://github.com/codedrifters/openhi/issues/1001\n */\nexport interface ProvisionOrganizationForWorkspaceParams {\n context: OpenHiContext;\n /** Workspace id; the provisioned Organization's id equals this value. */\n workspaceId: string;\n /** Name to record on the Organization. Derived from the Workspace's `name` by the caller. */\n workspaceName?: string;\n /** Optional table name override; resolved by data service from DYNAMO_TABLE_NAME when omitted. */\n tableName?: string;\n}\n\nexport type ProvisionOrganizationForWorkspaceResult =\n SingleResourceResult<Organization>;\n\nexport async function provisionOrganizationForWorkspaceOperation(\n params: ProvisionOrganizationForWorkspaceParams,\n): Promise<ProvisionOrganizationForWorkspaceResult> {\n const { context, workspaceId, workspaceName, tableName } = params;\n const { tenantId, date } = context;\n const service = getDynamoDataService(tableName);\n\n const partOf = await resolveTenantOrganizationReference(service, tenantId);\n\n const resource: Organization = {\n resourceType: \"Organization\",\n id: workspaceId,\n ...(workspaceName !== undefined && workspaceName !== \"\"\n ? { name: workspaceName }\n : {}),\n ...(partOf !== undefined ? { partOf } : {}),\n meta: {\n lastUpdated: date,\n versionId: \"1\",\n },\n };\n\n return createDataEntityRecord<Organization>(\n service.entities.organization as Parameters<\n typeof createDataEntityRecord\n >[0],\n tenantId,\n workspaceId,\n workspaceId,\n resource,\n date,\n );\n}\n\n/**\n * Resolve the Tenant's Organization reference for `Organization.partOf`. Returns `undefined`\n * when the Tenant has no Organization yet (the default until Tenant Organization provisioning\n * is wired up in a follow-up).\n *\n * Lookup convention: the Tenant's Organization is stored at\n * `(tenantId, workspaceId=tenantId, id=tenantId)` — a self-scope key that mirrors the\n * Workspace id-share pattern one level up. This scoping convention will be ratified by the\n * Tenant Organization provisioning issue; the OrganizationEntity comment notes strict\n * isolation requires both `tenantId` and `workspaceId`, so the self-scope is the\n * simplest deterministic location.\n */\nasync function resolveTenantOrganizationReference(\n service: ReturnType<typeof getDynamoDataService>,\n tenantId: string,\n): Promise<Reference | undefined> {\n const result = await (\n service.entities.organization as {\n get(params: {\n tenantId: string;\n workspaceId: string;\n id: string;\n sk: string;\n }): { go(): Promise<{ data: unknown }> };\n }\n )\n .get({\n tenantId,\n workspaceId: tenantId,\n id: tenantId,\n sk: DATA_ENTITY_SK,\n })\n .go();\n\n if (result.data === null || result.data === undefined) {\n return undefined;\n }\n\n return { reference: `Organization/${tenantId}` };\n}\n"],"mappings":"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","names":["extractSummary","extractSummary","extractReferenceSlug","extractSummary","extractSummary","extractSummary","extractSummary"]}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
createRoleAssignmentOperation,
|
|
15
15
|
createTenantOperation,
|
|
16
16
|
createWorkspaceOperation
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-7WDX6GPO.mjs";
|
|
18
18
|
import {
|
|
19
19
|
NotFoundError
|
|
20
20
|
} from "./chunk-FYHBHHWK.mjs";
|
|
@@ -932,4 +932,4 @@ export {
|
|
|
932
932
|
productionCognitoProvisioner,
|
|
933
933
|
handler
|
|
934
934
|
};
|
|
935
|
-
//# sourceMappingURL=chunk-
|
|
935
|
+
//# sourceMappingURL=chunk-WXS3PUHR.mjs.map
|
package/lib/index.d.mts
CHANGED
|
@@ -343,6 +343,15 @@ declare abstract class OpenHiService extends Stack {
|
|
|
343
343
|
* Short hash unique to the environment and branch combination.
|
|
344
344
|
*/
|
|
345
345
|
readonly branchHash: string;
|
|
346
|
+
/**
|
|
347
|
+
* Branch hash computed against {@link defaultReleaseBranch} rather than
|
|
348
|
+
* {@link branchName}. On the release branch this equals {@link branchHash};
|
|
349
|
+
* on every other branch it identifies the namespace the release-branch
|
|
350
|
+
* deploy of this service writes to. Use when looking up SSM parameters
|
|
351
|
+
* that only the release-branch stack publishes (e.g. shared static-hosting
|
|
352
|
+
* bucket ARN).
|
|
353
|
+
*/
|
|
354
|
+
readonly releaseBranchHash: string;
|
|
346
355
|
/**
|
|
347
356
|
* Short hash unique to the specific stack/service.
|
|
348
357
|
*/
|
|
@@ -1007,6 +1016,12 @@ declare class DiscoverableStringParameter extends StringParameter {
|
|
|
1007
1016
|
* Props for the StaticContent construct.
|
|
1008
1017
|
*/
|
|
1009
1018
|
interface StaticContentProps {
|
|
1019
|
+
/**
|
|
1020
|
+
* Destination bucket the content is uploaded to. Callers resolve this
|
|
1021
|
+
* reference themselves so the construct doesn't need to know whether the
|
|
1022
|
+
* bucket was created in the same stack or imported across branches.
|
|
1023
|
+
*/
|
|
1024
|
+
readonly bucket: IBucket;
|
|
1010
1025
|
/**
|
|
1011
1026
|
* Absolute path to directory containing content for the website.
|
|
1012
1027
|
*/
|
|
@@ -1031,21 +1046,14 @@ interface StaticContentProps {
|
|
|
1031
1046
|
* `<sub-domain>.<full-domain>`.
|
|
1032
1047
|
*/
|
|
1033
1048
|
readonly fullDomain: string;
|
|
1034
|
-
/**
|
|
1035
|
-
* Service type used to look up the static-hosting bucket ARN via
|
|
1036
|
-
* DiscoverableStringParameter.
|
|
1037
|
-
*
|
|
1038
|
-
* @default STATIC_HOSTING_SERVICE_TYPE ("website")
|
|
1039
|
-
*/
|
|
1040
|
-
readonly serviceType?: string;
|
|
1041
1049
|
}
|
|
1042
1050
|
/**
|
|
1043
|
-
* Static content uploader: deploys a local directory to
|
|
1051
|
+
* Static content uploader: deploys a local directory to a static-hosting
|
|
1044
1052
|
* S3 bucket under `<sub-domain>.<full-domain>/<dest>` so each branch
|
|
1045
|
-
* deploys to its own prefix without clobbering siblings. The
|
|
1046
|
-
*
|
|
1047
|
-
*
|
|
1048
|
-
*
|
|
1053
|
+
* deploys to its own prefix without clobbering siblings. The destination
|
|
1054
|
+
* bucket is supplied by the caller, which lets the same construct run in
|
|
1055
|
+
* both same-stack (release-branch) and cross-stack/cross-branch
|
|
1056
|
+
* (feature-branch) contexts.
|
|
1049
1057
|
*/
|
|
1050
1058
|
declare class StaticContent extends Construct {
|
|
1051
1059
|
constructor(scope: Construct, id: string, props: StaticContentProps);
|
|
@@ -1956,6 +1964,17 @@ interface OpenHiWebsiteServiceProps extends OpenHiServiceProps {
|
|
|
1956
1964
|
* @default - true on release branch, false otherwise
|
|
1957
1965
|
*/
|
|
1958
1966
|
readonly createHostingInfrastructure?: boolean;
|
|
1967
|
+
/**
|
|
1968
|
+
* Whether to create the `StaticContent` uploader. Set to `false` to skip
|
|
1969
|
+
* it entirely on every branch — used as a one-shot bootstrap toggle while
|
|
1970
|
+
* the release-branch deploy of this service first creates the static-hosting
|
|
1971
|
+
* bucket and writes `STATIC_HOSTING_BUCKET_ARN` to SSM. Once that
|
|
1972
|
+
* parameter exists, flip back to `true` so feature-branch deploys can read
|
|
1973
|
+
* it and upload content under their per-branch sub-domain folder.
|
|
1974
|
+
*
|
|
1975
|
+
* @default true
|
|
1976
|
+
*/
|
|
1977
|
+
readonly createStaticContent?: boolean;
|
|
1959
1978
|
}
|
|
1960
1979
|
/**
|
|
1961
1980
|
* SSM parameter name suffix for the website's full domain
|
|
@@ -2012,16 +2031,23 @@ declare class OpenHiWebsiteService extends OpenHiService {
|
|
|
2012
2031
|
*/
|
|
2013
2032
|
readonly staticHosting?: StaticHosting;
|
|
2014
2033
|
/**
|
|
2015
|
-
* The content uploader
|
|
2034
|
+
* The content uploader. Created on every deploy unless
|
|
2035
|
+
* {@link OpenHiWebsiteServiceProps.createStaticContent} is `false`, in
|
|
2036
|
+
* which case the property is `undefined` and the stack ships no
|
|
2037
|
+
* `BucketDeployment`. Used during release-branch bootstrap, before the
|
|
2038
|
+
* shared static-hosting bucket has been written to SSM for the first time.
|
|
2016
2039
|
*/
|
|
2017
|
-
readonly staticContent
|
|
2040
|
+
readonly staticContent?: StaticContent;
|
|
2018
2041
|
constructor(ohEnv: OpenHiEnvironment, props: OpenHiWebsiteServiceProps);
|
|
2019
2042
|
/**
|
|
2020
2043
|
* Validates that config required for the website stack is present.
|
|
2021
2044
|
*/
|
|
2022
2045
|
protected validateConfig(props: OpenHiWebsiteServiceProps): void;
|
|
2023
2046
|
/**
|
|
2024
|
-
*
|
|
2047
|
+
* Imports the website's hosted zone from config attributes (no SSM lookup).
|
|
2048
|
+
* The website attaches DNS records here on the release-branch deploy and
|
|
2049
|
+
* the same zone is imported on feature-branch deploys for any sub-domain
|
|
2050
|
+
* routing.
|
|
2025
2051
|
* Override to customize.
|
|
2026
2052
|
*/
|
|
2027
2053
|
protected createHostedZone(): IHostedZone;
|
|
@@ -2052,8 +2078,23 @@ declare class OpenHiWebsiteService extends OpenHiService {
|
|
|
2052
2078
|
* Creates the StaticContent uploader. Always created so feature-branch
|
|
2053
2079
|
* deploys can publish content to their own sub-domain folder against the
|
|
2054
2080
|
* release-branch bucket.
|
|
2081
|
+
*
|
|
2082
|
+
* The destination bucket is resolved here so the construct never has to
|
|
2083
|
+
* branch on release-vs-feature: on the release branch we pass the
|
|
2084
|
+
* just-created {@link staticHosting} bucket directly (no SSM round-trip
|
|
2085
|
+
* within a single stack); on every other branch we look up the bucket
|
|
2086
|
+
* ARN published by the release-branch deploy, addressed against
|
|
2087
|
+
* {@link OpenHiService.releaseBranchHash}.
|
|
2055
2088
|
*/
|
|
2056
2089
|
protected createStaticContent(): StaticContent;
|
|
2090
|
+
/**
|
|
2091
|
+
* Returns an {@link IBucket} pointing at the static-hosting bucket the
|
|
2092
|
+
* uploader writes to. On the release-branch deploy this is the bucket
|
|
2093
|
+
* just provisioned by {@link staticHosting}; on every other deploy it's
|
|
2094
|
+
* imported from the bucket ARN the release-branch deploy publishes to
|
|
2095
|
+
* SSM, addressed against {@link OpenHiService.releaseBranchHash}.
|
|
2096
|
+
*/
|
|
2097
|
+
protected resolveStaticHostingBucket(): IBucket;
|
|
2057
2098
|
}
|
|
2058
2099
|
|
|
2059
2100
|
interface OwningDeleteCascadeLambdasProps {
|
package/lib/index.d.ts
CHANGED
|
@@ -980,6 +980,15 @@ declare abstract class OpenHiService extends Stack {
|
|
|
980
980
|
* Short hash unique to the environment and branch combination.
|
|
981
981
|
*/
|
|
982
982
|
readonly branchHash: string;
|
|
983
|
+
/**
|
|
984
|
+
* Branch hash computed against {@link defaultReleaseBranch} rather than
|
|
985
|
+
* {@link branchName}. On the release branch this equals {@link branchHash};
|
|
986
|
+
* on every other branch it identifies the namespace the release-branch
|
|
987
|
+
* deploy of this service writes to. Use when looking up SSM parameters
|
|
988
|
+
* that only the release-branch stack publishes (e.g. shared static-hosting
|
|
989
|
+
* bucket ARN).
|
|
990
|
+
*/
|
|
991
|
+
readonly releaseBranchHash: string;
|
|
983
992
|
/**
|
|
984
993
|
* Short hash unique to the specific stack/service.
|
|
985
994
|
*/
|
|
@@ -1644,6 +1653,12 @@ declare class DiscoverableStringParameter extends StringParameter {
|
|
|
1644
1653
|
* Props for the StaticContent construct.
|
|
1645
1654
|
*/
|
|
1646
1655
|
interface StaticContentProps {
|
|
1656
|
+
/**
|
|
1657
|
+
* Destination bucket the content is uploaded to. Callers resolve this
|
|
1658
|
+
* reference themselves so the construct doesn't need to know whether the
|
|
1659
|
+
* bucket was created in the same stack or imported across branches.
|
|
1660
|
+
*/
|
|
1661
|
+
readonly bucket: IBucket;
|
|
1647
1662
|
/**
|
|
1648
1663
|
* Absolute path to directory containing content for the website.
|
|
1649
1664
|
*/
|
|
@@ -1668,21 +1683,14 @@ interface StaticContentProps {
|
|
|
1668
1683
|
* `<sub-domain>.<full-domain>`.
|
|
1669
1684
|
*/
|
|
1670
1685
|
readonly fullDomain: string;
|
|
1671
|
-
/**
|
|
1672
|
-
* Service type used to look up the static-hosting bucket ARN via
|
|
1673
|
-
* DiscoverableStringParameter.
|
|
1674
|
-
*
|
|
1675
|
-
* @default STATIC_HOSTING_SERVICE_TYPE ("website")
|
|
1676
|
-
*/
|
|
1677
|
-
readonly serviceType?: string;
|
|
1678
1686
|
}
|
|
1679
1687
|
/**
|
|
1680
|
-
* Static content uploader: deploys a local directory to
|
|
1688
|
+
* Static content uploader: deploys a local directory to a static-hosting
|
|
1681
1689
|
* S3 bucket under `<sub-domain>.<full-domain>/<dest>` so each branch
|
|
1682
|
-
* deploys to its own prefix without clobbering siblings. The
|
|
1683
|
-
*
|
|
1684
|
-
*
|
|
1685
|
-
*
|
|
1690
|
+
* deploys to its own prefix without clobbering siblings. The destination
|
|
1691
|
+
* bucket is supplied by the caller, which lets the same construct run in
|
|
1692
|
+
* both same-stack (release-branch) and cross-stack/cross-branch
|
|
1693
|
+
* (feature-branch) contexts.
|
|
1686
1694
|
*/
|
|
1687
1695
|
declare class StaticContent extends Construct {
|
|
1688
1696
|
constructor(scope: Construct, id: string, props: StaticContentProps);
|
|
@@ -2593,6 +2601,17 @@ interface OpenHiWebsiteServiceProps extends OpenHiServiceProps {
|
|
|
2593
2601
|
* @default - true on release branch, false otherwise
|
|
2594
2602
|
*/
|
|
2595
2603
|
readonly createHostingInfrastructure?: boolean;
|
|
2604
|
+
/**
|
|
2605
|
+
* Whether to create the `StaticContent` uploader. Set to `false` to skip
|
|
2606
|
+
* it entirely on every branch — used as a one-shot bootstrap toggle while
|
|
2607
|
+
* the release-branch deploy of this service first creates the static-hosting
|
|
2608
|
+
* bucket and writes `STATIC_HOSTING_BUCKET_ARN` to SSM. Once that
|
|
2609
|
+
* parameter exists, flip back to `true` so feature-branch deploys can read
|
|
2610
|
+
* it and upload content under their per-branch sub-domain folder.
|
|
2611
|
+
*
|
|
2612
|
+
* @default true
|
|
2613
|
+
*/
|
|
2614
|
+
readonly createStaticContent?: boolean;
|
|
2596
2615
|
}
|
|
2597
2616
|
/**
|
|
2598
2617
|
* SSM parameter name suffix for the website's full domain
|
|
@@ -2649,16 +2668,23 @@ declare class OpenHiWebsiteService extends OpenHiService {
|
|
|
2649
2668
|
*/
|
|
2650
2669
|
readonly staticHosting?: StaticHosting;
|
|
2651
2670
|
/**
|
|
2652
|
-
* The content uploader
|
|
2671
|
+
* The content uploader. Created on every deploy unless
|
|
2672
|
+
* {@link OpenHiWebsiteServiceProps.createStaticContent} is `false`, in
|
|
2673
|
+
* which case the property is `undefined` and the stack ships no
|
|
2674
|
+
* `BucketDeployment`. Used during release-branch bootstrap, before the
|
|
2675
|
+
* shared static-hosting bucket has been written to SSM for the first time.
|
|
2653
2676
|
*/
|
|
2654
|
-
readonly staticContent
|
|
2677
|
+
readonly staticContent?: StaticContent;
|
|
2655
2678
|
constructor(ohEnv: OpenHiEnvironment, props: OpenHiWebsiteServiceProps);
|
|
2656
2679
|
/**
|
|
2657
2680
|
* Validates that config required for the website stack is present.
|
|
2658
2681
|
*/
|
|
2659
2682
|
protected validateConfig(props: OpenHiWebsiteServiceProps): void;
|
|
2660
2683
|
/**
|
|
2661
|
-
*
|
|
2684
|
+
* Imports the website's hosted zone from config attributes (no SSM lookup).
|
|
2685
|
+
* The website attaches DNS records here on the release-branch deploy and
|
|
2686
|
+
* the same zone is imported on feature-branch deploys for any sub-domain
|
|
2687
|
+
* routing.
|
|
2662
2688
|
* Override to customize.
|
|
2663
2689
|
*/
|
|
2664
2690
|
protected createHostedZone(): IHostedZone;
|
|
@@ -2689,8 +2715,23 @@ declare class OpenHiWebsiteService extends OpenHiService {
|
|
|
2689
2715
|
* Creates the StaticContent uploader. Always created so feature-branch
|
|
2690
2716
|
* deploys can publish content to their own sub-domain folder against the
|
|
2691
2717
|
* release-branch bucket.
|
|
2718
|
+
*
|
|
2719
|
+
* The destination bucket is resolved here so the construct never has to
|
|
2720
|
+
* branch on release-vs-feature: on the release branch we pass the
|
|
2721
|
+
* just-created {@link staticHosting} bucket directly (no SSM round-trip
|
|
2722
|
+
* within a single stack); on every other branch we look up the bucket
|
|
2723
|
+
* ARN published by the release-branch deploy, addressed against
|
|
2724
|
+
* {@link OpenHiService.releaseBranchHash}.
|
|
2692
2725
|
*/
|
|
2693
2726
|
protected createStaticContent(): StaticContent;
|
|
2727
|
+
/**
|
|
2728
|
+
* Returns an {@link IBucket} pointing at the static-hosting bucket the
|
|
2729
|
+
* uploader writes to. On the release-branch deploy this is the bucket
|
|
2730
|
+
* just provisioned by {@link staticHosting}; on every other deploy it's
|
|
2731
|
+
* imported from the bucket ARN the release-branch deploy publishes to
|
|
2732
|
+
* SSM, addressed against {@link OpenHiService.releaseBranchHash}.
|
|
2733
|
+
*/
|
|
2734
|
+
protected resolveStaticHostingBucket(): IBucket;
|
|
2694
2735
|
}
|
|
2695
2736
|
|
|
2696
2737
|
interface OwningDeleteCascadeLambdasProps {
|
package/lib/index.js
CHANGED
|
@@ -1127,6 +1127,16 @@ var OpenHiService = class extends import_aws_cdk_lib4.Stack {
|
|
|
1127
1127
|
),
|
|
1128
1128
|
6
|
|
1129
1129
|
);
|
|
1130
|
+
const releaseBranchHash = (0, import_utils.hashString)(
|
|
1131
|
+
[
|
|
1132
|
+
appName,
|
|
1133
|
+
ohEnv.deploymentTargetRole,
|
|
1134
|
+
account,
|
|
1135
|
+
region,
|
|
1136
|
+
defaultReleaseBranch
|
|
1137
|
+
].join("-"),
|
|
1138
|
+
6
|
|
1139
|
+
);
|
|
1130
1140
|
const stackHash = (0, import_utils.hashString)(
|
|
1131
1141
|
[
|
|
1132
1142
|
appName,
|
|
@@ -1157,6 +1167,7 @@ var OpenHiService = class extends import_aws_cdk_lib4.Stack {
|
|
|
1157
1167
|
this.branchName = branchName;
|
|
1158
1168
|
this.environmentHash = environmentHash;
|
|
1159
1169
|
this.branchHash = branchHash;
|
|
1170
|
+
this.releaseBranchHash = releaseBranchHash;
|
|
1160
1171
|
this.stackHash = stackHash;
|
|
1161
1172
|
this.node.setContext(
|
|
1162
1173
|
`availability-zones:account=${account}:region=${region}`,
|
|
@@ -2286,10 +2297,31 @@ var RootHostedZone = class extends import_constructs7.Construct {
|
|
|
2286
2297
|
};
|
|
2287
2298
|
|
|
2288
2299
|
// src/components/static-hosting/static-content.ts
|
|
2289
|
-
var import_aws_s32 = require("aws-cdk-lib/aws-s3");
|
|
2290
2300
|
var import_aws_s3_deployment = require("aws-cdk-lib/aws-s3-deployment");
|
|
2291
2301
|
var import_change_case2 = require("change-case");
|
|
2292
|
-
var
|
|
2302
|
+
var import_constructs8 = require("constructs");
|
|
2303
|
+
var StaticContent = class extends import_constructs8.Construct {
|
|
2304
|
+
constructor(scope, id, props) {
|
|
2305
|
+
super(scope, id);
|
|
2306
|
+
const stack = OpenHiService.of(scope);
|
|
2307
|
+
const {
|
|
2308
|
+
bucket,
|
|
2309
|
+
contentSourceDirectory,
|
|
2310
|
+
contentDestinationDirectory = "/",
|
|
2311
|
+
subDomain = stack.branchName,
|
|
2312
|
+
fullDomain
|
|
2313
|
+
} = props;
|
|
2314
|
+
const keyPrefix = [(0, import_change_case2.paramCase)(subDomain), fullDomain].join(".");
|
|
2315
|
+
const isTestEnv = process.env.JEST_WORKER_ID !== void 0;
|
|
2316
|
+
const sources = isTestEnv ? [] : [import_aws_s3_deployment.Source.asset(contentSourceDirectory)];
|
|
2317
|
+
new import_aws_s3_deployment.BucketDeployment(this, "deploy", {
|
|
2318
|
+
sources,
|
|
2319
|
+
destinationBucket: bucket,
|
|
2320
|
+
retainOnDelete: false,
|
|
2321
|
+
destinationKeyPrefix: `${keyPrefix}${contentDestinationDirectory}`
|
|
2322
|
+
});
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2293
2325
|
|
|
2294
2326
|
// src/components/static-hosting/static-hosting.ts
|
|
2295
2327
|
var fs6 = __toESM(require("fs"));
|
|
@@ -2303,9 +2335,9 @@ var import_aws_logs = require("aws-cdk-lib/aws-logs");
|
|
|
2303
2335
|
var import_aws_route532 = require("aws-cdk-lib/aws-route53");
|
|
2304
2336
|
var import_aws_route53_targets = require("aws-cdk-lib/aws-route53-targets");
|
|
2305
2337
|
var import_aws_s3 = require("aws-cdk-lib/aws-s3");
|
|
2306
|
-
var
|
|
2338
|
+
var import_constructs9 = require("constructs");
|
|
2307
2339
|
var STATIC_HOSTING_SERVICE_TYPE = "website";
|
|
2308
|
-
var _StaticHosting = class _StaticHosting extends
|
|
2340
|
+
var _StaticHosting = class _StaticHosting extends import_constructs9.Construct {
|
|
2309
2341
|
constructor(scope, id, props = {}) {
|
|
2310
2342
|
super(scope, id);
|
|
2311
2343
|
const stack = OpenHiService.of(scope);
|
|
@@ -2441,35 +2473,6 @@ _StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_DOMAIN = "STATIC_HOSTING_DISTRIBUTION
|
|
|
2441
2473
|
_StaticHosting.SSM_PARAM_NAME_DISTRIBUTION_ID = "STATIC_HOSTING_DISTRIBUTION_ID";
|
|
2442
2474
|
var StaticHosting = _StaticHosting;
|
|
2443
2475
|
|
|
2444
|
-
// src/components/static-hosting/static-content.ts
|
|
2445
|
-
var StaticContent = class extends import_constructs9.Construct {
|
|
2446
|
-
constructor(scope, id, props) {
|
|
2447
|
-
super(scope, id);
|
|
2448
|
-
const stack = OpenHiService.of(scope);
|
|
2449
|
-
const {
|
|
2450
|
-
contentSourceDirectory,
|
|
2451
|
-
contentDestinationDirectory = "/",
|
|
2452
|
-
subDomain = stack.branchName,
|
|
2453
|
-
fullDomain,
|
|
2454
|
-
serviceType = STATIC_HOSTING_SERVICE_TYPE
|
|
2455
|
-
} = props;
|
|
2456
|
-
const keyPrefix = [(0, import_change_case2.paramCase)(subDomain), fullDomain].join(".");
|
|
2457
|
-
const bucketArn = DiscoverableStringParameter.valueForLookupName(this, {
|
|
2458
|
-
ssmParamName: StaticHosting.SSM_PARAM_NAME_BUCKET_ARN,
|
|
2459
|
-
serviceType
|
|
2460
|
-
});
|
|
2461
|
-
const bucket = import_aws_s32.Bucket.fromBucketArn(this, "bucket", bucketArn);
|
|
2462
|
-
const isTestEnv = process.env.JEST_WORKER_ID !== void 0;
|
|
2463
|
-
const sources = isTestEnv ? [] : [import_aws_s3_deployment.Source.asset(contentSourceDirectory)];
|
|
2464
|
-
new import_aws_s3_deployment.BucketDeployment(this, "deploy", {
|
|
2465
|
-
sources,
|
|
2466
|
-
destinationBucket: bucket,
|
|
2467
|
-
retainOnDelete: false,
|
|
2468
|
-
destinationKeyPrefix: `${keyPrefix}${contentDestinationDirectory}`
|
|
2469
|
-
});
|
|
2470
|
-
}
|
|
2471
|
-
};
|
|
2472
|
-
|
|
2473
2476
|
// src/services/open-hi-auth-service.ts
|
|
2474
2477
|
var import_aws_cognito4 = require("aws-cdk-lib/aws-cognito");
|
|
2475
2478
|
var import_aws_iam6 = require("aws-cdk-lib/aws-iam");
|
|
@@ -7180,6 +7183,7 @@ _OpenHiGraphqlService.SERVICE_TYPE = "graphql-api";
|
|
|
7180
7183
|
var OpenHiGraphqlService = _OpenHiGraphqlService;
|
|
7181
7184
|
|
|
7182
7185
|
// src/services/open-hi-website-service.ts
|
|
7186
|
+
var import_aws_s32 = require("aws-cdk-lib/aws-s3");
|
|
7183
7187
|
var SSM_PARAM_NAME_FULL_DOMAIN = "WEBSITE_FULL_DOMAIN";
|
|
7184
7188
|
var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
|
|
7185
7189
|
/**
|
|
@@ -7250,7 +7254,9 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
|
|
|
7250
7254
|
});
|
|
7251
7255
|
this.createFullDomainParameter();
|
|
7252
7256
|
}
|
|
7253
|
-
|
|
7257
|
+
if (props.createStaticContent !== false) {
|
|
7258
|
+
this.staticContent = this.createStaticContent();
|
|
7259
|
+
}
|
|
7254
7260
|
}
|
|
7255
7261
|
/**
|
|
7256
7262
|
* Validates that config required for the website stack is present.
|
|
@@ -7263,14 +7269,21 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
|
|
|
7263
7269
|
if (!config.zoneName) {
|
|
7264
7270
|
throw new Error("Zone name is required");
|
|
7265
7271
|
}
|
|
7272
|
+
if (!config.hostedZoneId) {
|
|
7273
|
+
throw new Error("Hosted zone ID is required to import the website zone");
|
|
7274
|
+
}
|
|
7266
7275
|
}
|
|
7267
7276
|
/**
|
|
7268
|
-
*
|
|
7277
|
+
* Imports the website's hosted zone from config attributes (no SSM lookup).
|
|
7278
|
+
* The website attaches DNS records here on the release-branch deploy and
|
|
7279
|
+
* the same zone is imported on feature-branch deploys for any sub-domain
|
|
7280
|
+
* routing.
|
|
7269
7281
|
* Override to customize.
|
|
7270
7282
|
*/
|
|
7271
7283
|
createHostedZone() {
|
|
7272
|
-
return OpenHiGlobalService.
|
|
7273
|
-
zoneName: this.config.zoneName
|
|
7284
|
+
return OpenHiGlobalService.rootHostedZoneFromConstruct(this, {
|
|
7285
|
+
zoneName: this.config.zoneName,
|
|
7286
|
+
hostedZoneId: this.config.hostedZoneId
|
|
7274
7287
|
});
|
|
7275
7288
|
}
|
|
7276
7289
|
/**
|
|
@@ -7317,15 +7330,40 @@ var _OpenHiWebsiteService = class _OpenHiWebsiteService extends OpenHiService {
|
|
|
7317
7330
|
* Creates the StaticContent uploader. Always created so feature-branch
|
|
7318
7331
|
* deploys can publish content to their own sub-domain folder against the
|
|
7319
7332
|
* release-branch bucket.
|
|
7333
|
+
*
|
|
7334
|
+
* The destination bucket is resolved here so the construct never has to
|
|
7335
|
+
* branch on release-vs-feature: on the release branch we pass the
|
|
7336
|
+
* just-created {@link staticHosting} bucket directly (no SSM round-trip
|
|
7337
|
+
* within a single stack); on every other branch we look up the bucket
|
|
7338
|
+
* ARN published by the release-branch deploy, addressed against
|
|
7339
|
+
* {@link OpenHiService.releaseBranchHash}.
|
|
7320
7340
|
*/
|
|
7321
7341
|
createStaticContent() {
|
|
7322
7342
|
const { contentSourceDirectory, contentDestinationDirectory } = this.props;
|
|
7323
7343
|
return new StaticContent(this, "static-content", {
|
|
7344
|
+
bucket: this.resolveStaticHostingBucket(),
|
|
7324
7345
|
contentSourceDirectory,
|
|
7325
7346
|
contentDestinationDirectory,
|
|
7326
|
-
fullDomain: this.fullDomain
|
|
7327
|
-
|
|
7347
|
+
fullDomain: this.fullDomain
|
|
7348
|
+
});
|
|
7349
|
+
}
|
|
7350
|
+
/**
|
|
7351
|
+
* Returns an {@link IBucket} pointing at the static-hosting bucket the
|
|
7352
|
+
* uploader writes to. On the release-branch deploy this is the bucket
|
|
7353
|
+
* just provisioned by {@link staticHosting}; on every other deploy it's
|
|
7354
|
+
* imported from the bucket ARN the release-branch deploy publishes to
|
|
7355
|
+
* SSM, addressed against {@link OpenHiService.releaseBranchHash}.
|
|
7356
|
+
*/
|
|
7357
|
+
resolveStaticHostingBucket() {
|
|
7358
|
+
if (this.staticHosting) {
|
|
7359
|
+
return this.staticHosting.bucket;
|
|
7360
|
+
}
|
|
7361
|
+
const bucketArn = DiscoverableStringParameter.valueForLookupName(this, {
|
|
7362
|
+
ssmParamName: StaticHosting.SSM_PARAM_NAME_BUCKET_ARN,
|
|
7363
|
+
serviceType: _OpenHiWebsiteService.SERVICE_TYPE,
|
|
7364
|
+
branchHash: this.releaseBranchHash
|
|
7328
7365
|
});
|
|
7366
|
+
return import_aws_s32.Bucket.fromBucketArn(this, "shared-bucket", bucketArn);
|
|
7329
7367
|
}
|
|
7330
7368
|
};
|
|
7331
7369
|
_OpenHiWebsiteService.SERVICE_TYPE = "website";
|