@projectdochelp/s3te 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,301 @@
1
+ import { createAwsClients, invokeLambdaEvent } from "./common.mjs";
2
+
3
+ function escapeHtml(value) {
4
+ return String(value)
5
+ .replace(/&/g, "&")
6
+ .replace(/</g, "&lt;")
7
+ .replace(/>/g, "&gt;")
8
+ .replace(/\"/g, "&quot;")
9
+ .replace(/'/g, "&#39;");
10
+ }
11
+
12
+ function renderInlineTextNode(node) {
13
+ let value = escapeHtml(node.text ?? "");
14
+ const format = Number(node.format ?? 0);
15
+
16
+ if (format & 1) {
17
+ value = `<b>${value}</b>`;
18
+ }
19
+ if (format & 2) {
20
+ value = `<i>${value}</i>`;
21
+ }
22
+ if (format & 8) {
23
+ value = `<u>${value}</u>`;
24
+ }
25
+
26
+ return value;
27
+ }
28
+
29
+ function renderRichTextChildren(children = []) {
30
+ return children.map((child) => renderRichTextNode(child)).join("");
31
+ }
32
+
33
+ function renderRichTextAttributes(node) {
34
+ const attributes = [];
35
+ if (node.className) {
36
+ attributes.push(` class="${escapeHtml(node.className)}"`);
37
+ }
38
+ if (node.format && typeof node.format === "string") {
39
+ attributes.push(` style="text-align:${escapeHtml(node.format)}"`);
40
+ }
41
+ return attributes.join("");
42
+ }
43
+
44
+ function renderRichTextNode(node) {
45
+ if (!node || typeof node !== "object") {
46
+ return "";
47
+ }
48
+
49
+ if (node.type === "root") {
50
+ return renderRichTextChildren(node.children);
51
+ }
52
+ if (node.type === "text") {
53
+ return renderInlineTextNode(node);
54
+ }
55
+ if (node.type === "linebreak") {
56
+ return "<br>";
57
+ }
58
+ if (node.type === "delimiter") {
59
+ return "<hr>";
60
+ }
61
+ if (node.type === "paragraph-element") {
62
+ return `<p${renderRichTextAttributes(node)}>${renderRichTextChildren(node.children)}</p>`;
63
+ }
64
+ if (node.type === "heading-element") {
65
+ const tag = /^h[1-6]$/i.test(node.tag) ? node.tag.toLowerCase() : "h2";
66
+ return `<${tag}${renderRichTextAttributes(node)}>${renderRichTextChildren(node.children)}</${tag}>`;
67
+ }
68
+ if (node.type === "webiny-list") {
69
+ const tag = node.listType === "number" || node.format === "number" ? "ol" : "ul";
70
+ return `<${tag}>${renderRichTextChildren(node.children)}</${tag}>`;
71
+ }
72
+ if (node.type === "webiny-listitem") {
73
+ return `<li>${renderRichTextChildren(node.children)}</li>`;
74
+ }
75
+ if (node.type === "link") {
76
+ const href = node.url ?? node.href ?? "#";
77
+ const target = node.target ? ` target="${escapeHtml(node.target)}"` : "";
78
+ return `<a href="${escapeHtml(href)}"${target}>${renderRichTextChildren(node.children)}</a>`;
79
+ }
80
+ if (node.type === "image") {
81
+ const src = node.src ?? node.url ?? node.file?.src ?? node.file?.url ?? "";
82
+ if (!src) {
83
+ return "";
84
+ }
85
+ const alt = escapeHtml(node.altText ?? node.alt ?? "");
86
+ const caption = node.caption ? `<figcaption>${escapeHtml(node.caption)}</figcaption>` : "";
87
+ return `<figure><img src="${escapeHtml(src)}" alt="${alt}">${caption}</figure>`;
88
+ }
89
+
90
+ return renderRichTextChildren(node.children);
91
+ }
92
+
93
+ function serializeStructuredValue(value) {
94
+ if (value && typeof value === "object") {
95
+ if (Array.isArray(value.root?.children)) {
96
+ return renderRichTextNode(value.root);
97
+ }
98
+
99
+ if (typeof value.type === "string" && Array.isArray(value.children)) {
100
+ return renderRichTextNode(value);
101
+ }
102
+ }
103
+
104
+ return null;
105
+ }
106
+
107
+ function toSimpleValue(value) {
108
+ if (value == null) {
109
+ return null;
110
+ }
111
+
112
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
113
+ return value;
114
+ }
115
+
116
+ if (Array.isArray(value)) {
117
+ return value.map((entry) => String(toSimpleValue(entry)));
118
+ }
119
+
120
+ if (typeof value === "object") {
121
+ if (typeof value.html === "string") {
122
+ return value.html;
123
+ }
124
+ if (typeof value.text === "string") {
125
+ return value.text;
126
+ }
127
+
128
+ const structured = serializeStructuredValue(value);
129
+ if (structured !== null) {
130
+ return structured;
131
+ }
132
+ }
133
+
134
+ return String(value);
135
+ }
136
+
137
+ function normalizeValues(item) {
138
+ const valueSource = item.values && typeof item.values === "object"
139
+ ? item.values
140
+ : (item.data && typeof item.data === "object" && !Array.isArray(item.data) ? item.data : null);
141
+
142
+ if (valueSource) {
143
+ return Object.fromEntries(Object.entries(valueSource).map(([key, value]) => [key, toSimpleValue(value)]));
144
+ }
145
+
146
+ const reserved = new Set([
147
+ "id",
148
+ "entryId",
149
+ "contentId",
150
+ "contentid",
151
+ "model",
152
+ "modelId",
153
+ "__typename",
154
+ "locale",
155
+ "localeCode",
156
+ "tenant",
157
+ "tenantId",
158
+ "data",
159
+ "createdAt",
160
+ "createdOn",
161
+ "updatedAt",
162
+ "savedOn",
163
+ "publishedOn",
164
+ "_version",
165
+ "_lastChangedAt",
166
+ "status",
167
+ "published"
168
+ ]);
169
+
170
+ const values = {};
171
+ for (const [key, value] of Object.entries(item)) {
172
+ if (reserved.has(key) || key.startsWith("_")) {
173
+ continue;
174
+ }
175
+ values[key] = toSimpleValue(value);
176
+ }
177
+ return values;
178
+ }
179
+
180
+ function isPublished(item) {
181
+ return item.status === "published"
182
+ || item.published === true
183
+ || item.isPublished === true
184
+ || item.publishedOn != null;
185
+ }
186
+
187
+ function extractWebinyLocale(item) {
188
+ return item.locale
189
+ ?? item.localeCode
190
+ ?? item.i18n?.locale?.code
191
+ ?? item.i18n?.localeCode
192
+ ?? null;
193
+ }
194
+
195
+ function extractWebinyTenant(item) {
196
+ return item.tenant
197
+ ?? item.tenantId
198
+ ?? item.createdBy?.tenant
199
+ ?? null;
200
+ }
201
+
202
+ export function normalizeContentItem(item) {
203
+ const model = item.model
204
+ ?? item.modelId
205
+ ?? item.__typename
206
+ ?? item.contentModel?.modelId
207
+ ?? null;
208
+ return {
209
+ id: item.id,
210
+ contentId: item.contentId ?? item.contentid ?? item.entryId ?? item.id,
211
+ model,
212
+ locale: extractWebinyLocale(item) ?? undefined,
213
+ tenant: extractWebinyTenant(item) ?? undefined,
214
+ values: normalizeValues(item),
215
+ createdAt: item.createdAt ?? item.createdOn,
216
+ updatedAt: item.updatedAt ?? item.savedOn ?? item.publishedOn,
217
+ version: item._version ?? item.version,
218
+ lastChangedAt: item._lastChangedAt ?? item.lastChangedAt
219
+ };
220
+ }
221
+
222
+ export function matchesConfiguredTenant(item, configuredTenant) {
223
+ if (!configuredTenant) {
224
+ return true;
225
+ }
226
+
227
+ const tenant = item.tenant ?? item.tenantId ?? item.createdBy?.tenant ?? null;
228
+ return tenant != null && String(tenant) === String(configuredTenant);
229
+ }
230
+
231
+ export async function handler(event) {
232
+ const clients = createAwsClients();
233
+ const tableName = process.env.S3TE_CONTENT_TABLE;
234
+ const renderWorkerName = process.env.S3TE_RENDER_WORKER_NAME;
235
+ const environmentName = process.env.S3TE_ENVIRONMENT;
236
+ const configuredTenant = String(process.env.S3TE_WEBINY_TENANT ?? "").trim();
237
+ const relevantModels = new Set(String(process.env.S3TE_RELEVANT_MODELS ?? "")
238
+ .split(",")
239
+ .map((entry) => entry.trim())
240
+ .filter(Boolean));
241
+
242
+ let mirrored = 0;
243
+ let deleted = 0;
244
+
245
+ for (const record of event.Records ?? []) {
246
+ const image = record.dynamodb?.NewImage ?? record.dynamodb?.OldImage;
247
+ if (!image) {
248
+ continue;
249
+ }
250
+
251
+ const item = clients.AWS.DynamoDB.Converter.unmarshall(image);
252
+ if (!matchesConfiguredTenant(item, configuredTenant)) {
253
+ continue;
254
+ }
255
+
256
+ const contentItem = normalizeContentItem(item);
257
+ if (!contentItem.id || !contentItem.model || (relevantModels.size > 0 && !relevantModels.has(contentItem.model))) {
258
+ continue;
259
+ }
260
+
261
+ const shouldDelete = record.eventName === "REMOVE" || !isPublished(item);
262
+ if (shouldDelete) {
263
+ await clients.dynamo.delete({
264
+ TableName: tableName,
265
+ Key: {
266
+ id: contentItem.id
267
+ }
268
+ }).promise();
269
+ deleted += 1;
270
+ await invokeLambdaEvent(clients.lambda, renderWorkerName, {
271
+ type: "content-item",
272
+ action: "delete",
273
+ environment: environmentName,
274
+ contentId: contentItem.contentId,
275
+ model: contentItem.model,
276
+ buildId: `content-${Date.now()}`
277
+ });
278
+ continue;
279
+ }
280
+
281
+ await clients.dynamo.put({
282
+ TableName: tableName,
283
+ Item: contentItem
284
+ }).promise();
285
+ mirrored += 1;
286
+ await invokeLambdaEvent(clients.lambda, renderWorkerName, {
287
+ type: "content-item",
288
+ action: "upsert",
289
+ environment: environmentName,
290
+ contentId: contentItem.contentId,
291
+ model: contentItem.model,
292
+ item: contentItem,
293
+ buildId: `content-${Date.now()}`
294
+ });
295
+ }
296
+
297
+ return {
298
+ mirrored,
299
+ deleted
300
+ };
301
+ }
@@ -0,0 +1,61 @@
1
+ import { createAwsClients } from "./common.mjs";
2
+
3
+ function chunk(items, size) {
4
+ const chunks = [];
5
+ for (let index = 0; index < items.length; index += size) {
6
+ chunks.push(items.slice(index, index + size));
7
+ }
8
+ return chunks;
9
+ }
10
+
11
+ export async function handler(event) {
12
+ const clients = createAwsClients();
13
+ const tableName = process.env.S3TE_INVALIDATION_TABLE;
14
+ const distributionId = event.distributionId;
15
+
16
+ const response = await clients.dynamo.query({
17
+ TableName: tableName,
18
+ KeyConditionExpression: "distributionId = :distributionId",
19
+ ExpressionAttributeValues: {
20
+ ":distributionId": distributionId
21
+ }
22
+ }).promise();
23
+
24
+ const items = response.Items ?? [];
25
+ const requests = items.filter((item) => item.requestId !== "__window__" && item.type === "request");
26
+ if (requests.length > 0) {
27
+ await clients.cloudFront.createInvalidation({
28
+ DistributionId: distributionId,
29
+ InvalidationBatch: {
30
+ CallerReference: `${distributionId}-${Date.now()}`,
31
+ Paths: {
32
+ Quantity: 1,
33
+ Items: ["/*"]
34
+ }
35
+ }
36
+ }).promise();
37
+ }
38
+
39
+ const deletions = items.map((item) => ({
40
+ DeleteRequest: {
41
+ Key: {
42
+ distributionId: item.distributionId,
43
+ requestId: item.requestId
44
+ }
45
+ }
46
+ }));
47
+
48
+ for (const batch of chunk(deletions, 25)) {
49
+ await clients.dynamo.batchWrite({
50
+ RequestItems: {
51
+ [tableName]: batch
52
+ }
53
+ }).promise();
54
+ }
55
+
56
+ return {
57
+ distributionId,
58
+ invalidated: requests.length > 0,
59
+ deletedRecords: deletions.length
60
+ };
61
+ }
@@ -0,0 +1,59 @@
1
+ import { createAwsClients } from "./common.mjs";
2
+
3
+ export async function handler(event) {
4
+ const clients = createAwsClients();
5
+ const tableName = process.env.S3TE_INVALIDATION_TABLE;
6
+ const stateMachineArn = process.env.S3TE_INVALIDATION_STATE_MACHINE_ARN;
7
+
8
+ const requestId = `${event.requestedAt ?? new Date().toISOString()}#${event.buildId ?? "build"}`;
9
+ await clients.dynamo.put({
10
+ TableName: tableName,
11
+ Item: {
12
+ distributionId: event.distributionId,
13
+ requestId,
14
+ type: "request",
15
+ environment: event.environment,
16
+ variant: event.variant,
17
+ language: event.language,
18
+ distributionAliases: event.distributionAliases ?? [],
19
+ paths: event.paths ?? ["/*"],
20
+ requestedAt: event.requestedAt ?? new Date().toISOString(),
21
+ status: "pending"
22
+ }
23
+ }).promise();
24
+
25
+ let windowOpened = false;
26
+ try {
27
+ await clients.dynamo.put({
28
+ TableName: tableName,
29
+ Item: {
30
+ distributionId: event.distributionId,
31
+ requestId: "__window__",
32
+ type: "window",
33
+ windowOpenedAt: new Date().toISOString()
34
+ },
35
+ ConditionExpression: "attribute_not_exists(distributionId)"
36
+ }).promise();
37
+ windowOpened = true;
38
+ } catch (error) {
39
+ const errorCode = error?.name ?? error?.Code ?? error?.code;
40
+ if (errorCode !== "ConditionalCheckFailedException") {
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ if (windowOpened) {
46
+ await clients.stepFunctions.startExecution({
47
+ stateMachineArn,
48
+ input: JSON.stringify({
49
+ distributionId: event.distributionId
50
+ })
51
+ }).promise();
52
+ }
53
+
54
+ return {
55
+ distributionId: event.distributionId,
56
+ requestId,
57
+ windowOpened
58
+ };
59
+ }
@@ -0,0 +1,83 @@
1
+ import {
2
+ createAwsClients,
3
+ createBuildId,
4
+ createRepositoriesAndPublishers,
5
+ deleteOutputsForTemplate,
6
+ loadEnvironmentManifest,
7
+ renderAndPublishTargets,
8
+ resolveRenderTargetsForEvent,
9
+ S3TemplateRepository
10
+ } from "./common.mjs";
11
+
12
+ export async function handler(event) {
13
+ const environmentName = process.env.S3TE_ENVIRONMENT;
14
+ const runtimeParameter = process.env.S3TE_RUNTIME_PARAMETER;
15
+ const clients = createAwsClients();
16
+ const { manifest, environment: environmentManifest } = await loadEnvironmentManifest(
17
+ clients.ssm,
18
+ runtimeParameter,
19
+ environmentName
20
+ );
21
+ const {
22
+ contentRepository,
23
+ dependencyStore,
24
+ publisher,
25
+ invalidationScheduler
26
+ } = createRepositoriesAndPublishers({
27
+ clients,
28
+ environmentManifest
29
+ });
30
+
31
+ const buildId = event.buildId ?? createBuildId("render");
32
+
33
+ if (event.type === "source-object" && event.action === "delete" && event.key.startsWith(`${event.variant}/`)) {
34
+ const deletedOutputs = await deleteOutputsForTemplate({
35
+ event,
36
+ environmentName,
37
+ environmentManifest,
38
+ dependencyStore,
39
+ publisher,
40
+ invalidationScheduler,
41
+ buildId
42
+ });
43
+
44
+ return {
45
+ buildId,
46
+ rendered: [],
47
+ deleted: deletedOutputs,
48
+ warnings: []
49
+ };
50
+ }
51
+
52
+ const targets = await resolveRenderTargetsForEvent({
53
+ event,
54
+ manifest,
55
+ environmentName,
56
+ environmentManifest,
57
+ dependencyStore,
58
+ templateRepositoryFactory: (variantName) => new S3TemplateRepository({
59
+ s3: clients.s3,
60
+ environmentManifest,
61
+ activeVariantName: variantName
62
+ })
63
+ });
64
+
65
+ const result = await renderAndPublishTargets({
66
+ manifest,
67
+ environmentName,
68
+ environmentManifest,
69
+ contentRepository,
70
+ dependencyStore,
71
+ publisher,
72
+ invalidationScheduler,
73
+ targets,
74
+ buildId
75
+ });
76
+
77
+ return {
78
+ buildId,
79
+ rendered: result.rendered,
80
+ deleted: result.deleted,
81
+ warnings: result.warnings
82
+ };
83
+ }
@@ -0,0 +1,106 @@
1
+ import {
2
+ buildInvalidationRequest,
3
+ createAwsClients,
4
+ createBuildId,
5
+ decodeS3Key,
6
+ invokeLambdaEvent,
7
+ isRenderableBucketKey,
8
+ loadEnvironmentManifest
9
+ } from "./common.mjs";
10
+
11
+ function variantFromBucket(environmentManifest, bucketName) {
12
+ return Object.keys(environmentManifest.variants).find((variantName) => (
13
+ environmentManifest.variants[variantName].codeBucket === bucketName
14
+ ));
15
+ }
16
+
17
+ function outputKeyFromAssetKey(variantName, key) {
18
+ if (key.startsWith(`${variantName}/`)) {
19
+ return key.slice(variantName.length + 1);
20
+ }
21
+ return key;
22
+ }
23
+
24
+ export async function handler(event) {
25
+ const environmentName = process.env.S3TE_ENVIRONMENT;
26
+ const runtimeParameter = process.env.S3TE_RUNTIME_PARAMETER;
27
+ const renderWorkerName = process.env.S3TE_RENDER_WORKER_NAME;
28
+ const renderExtensions = String(process.env.S3TE_RENDER_EXTENSIONS ?? "")
29
+ .split(",")
30
+ .map((entry) => entry.trim())
31
+ .filter(Boolean);
32
+
33
+ const clients = createAwsClients();
34
+ const { environment: environmentManifest } = await loadEnvironmentManifest(
35
+ clients.ssm,
36
+ runtimeParameter,
37
+ environmentName
38
+ );
39
+
40
+ let copiedAssets = 0;
41
+ let deletedAssets = 0;
42
+ let dispatchedBuilds = 0;
43
+
44
+ for (const record of event.Records ?? []) {
45
+ const bucketName = record.s3?.bucket?.name;
46
+ const key = decodeS3Key(record.s3?.object?.key ?? "");
47
+ const variantName = variantFromBucket(environmentManifest, bucketName);
48
+ if (!variantName || !key) {
49
+ continue;
50
+ }
51
+
52
+ const action = String(record.eventName).startsWith("ObjectRemoved:") ? "delete" : "upsert";
53
+ const buildId = createBuildId("source");
54
+
55
+ if (isRenderableBucketKey(environmentManifest, variantName, key, renderExtensions)) {
56
+ await invokeLambdaEvent(clients.lambda, renderWorkerName, {
57
+ type: "source-object",
58
+ action,
59
+ bucket: bucketName,
60
+ key,
61
+ environment: environmentName,
62
+ variant: variantName,
63
+ buildId
64
+ });
65
+ dispatchedBuilds += 1;
66
+ continue;
67
+ }
68
+
69
+ const outputKey = outputKeyFromAssetKey(variantName, key);
70
+ for (const languageCode of Object.keys(environmentManifest.variants[variantName].languages)) {
71
+ const targetBucket = environmentManifest.variants[variantName].languages[languageCode].targetBucket;
72
+ if (action === "upsert") {
73
+ await clients.s3.copyObject({
74
+ Bucket: targetBucket,
75
+ Key: outputKey,
76
+ CopySource: `${bucketName}/${key.split("/").map(encodeURIComponent).join("/")}`
77
+ }).promise();
78
+ copiedAssets += 1;
79
+ } else {
80
+ await clients.s3.deleteObject({
81
+ Bucket: targetBucket,
82
+ Key: outputKey
83
+ }).promise();
84
+ deletedAssets += 1;
85
+ }
86
+
87
+ await invokeLambdaEvent(
88
+ clients.lambda,
89
+ environmentManifest.functions.invalidationScheduler,
90
+ buildInvalidationRequest(
91
+ environmentName,
92
+ environmentManifest,
93
+ variantName,
94
+ languageCode,
95
+ buildId
96
+ )
97
+ );
98
+ }
99
+ }
100
+
101
+ return {
102
+ copiedAssets,
103
+ deletedAssets,
104
+ dispatchedBuilds
105
+ };
106
+ }