@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,464 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { assert, S3teError } from "./errors.mjs";
5
+
6
+ const KNOWN_PLACEHOLDERS = new Set(["env", "stackPrefix", "project", "variant", "lang"]);
7
+
8
+ function upperSnakeCase(value) {
9
+ return value.replace(/-/g, "_").toUpperCase();
10
+ }
11
+
12
+ function findPlaceholders(input) {
13
+ return [...String(input).matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]);
14
+ }
15
+
16
+ function ensureKnownPlaceholders(input, fieldPath, errors) {
17
+ for (const token of findPlaceholders(input)) {
18
+ if (KNOWN_PLACEHOLDERS.has(token)) {
19
+ continue;
20
+ }
21
+
22
+ errors.push({
23
+ code: "CONFIG_PLACEHOLDER_ERROR",
24
+ message: `Unknown placeholder {${token}} in ${fieldPath}.`,
25
+ details: { fieldPath, token }
26
+ });
27
+ }
28
+ }
29
+
30
+ function replacePlaceholders(input, values) {
31
+ return String(input).replace(/\{(env|stackPrefix|project|variant|lang)\}/g, (_, token) => values[token]);
32
+ }
33
+
34
+ function normalizeRelativeProjectPath(relativePath) {
35
+ const normalized = String(relativePath).replace(/\\/g, "/");
36
+ assert(!path.isAbsolute(normalized), "CONFIG_PATH_ERROR", "Absolute project paths are not allowed.", { relativePath });
37
+ assert(!normalized.split("/").includes(".."), "CONFIG_PATH_ERROR", "Parent traversal is not allowed in project paths.", { relativePath });
38
+ return normalized;
39
+ }
40
+
41
+ function isValidProjectName(name) {
42
+ return /^[a-z0-9-]+$/.test(name);
43
+ }
44
+
45
+ function isValidUpperSnake(value) {
46
+ return /^[A-Z0-9_]+$/.test(value);
47
+ }
48
+
49
+ function defaultTargetBucketPattern({ variant, language, languageCount, isDefaultLanguage, project }) {
50
+ if (languageCount === 1 || isDefaultLanguage) {
51
+ return `{env}-${variant}-${project}`;
52
+ }
53
+
54
+ return `{env}-${variant}-${project}-${language}`;
55
+ }
56
+
57
+ async function ensureDirectoryExists(projectDir, relativePath, errors) {
58
+ const targetPath = path.join(projectDir, relativePath);
59
+ try {
60
+ const stat = await fs.stat(targetPath);
61
+ if (!stat.isDirectory()) {
62
+ errors.push({
63
+ code: "CONFIG_PATH_ERROR",
64
+ message: `Expected directory but found file: ${relativePath}`,
65
+ details: { relativePath }
66
+ });
67
+ }
68
+ } catch {
69
+ errors.push({
70
+ code: "CONFIG_PATH_ERROR",
71
+ message: `Missing directory: ${relativePath}`,
72
+ details: { relativePath }
73
+ });
74
+ }
75
+ }
76
+
77
+ function createPlaceholderContext(config, environmentName, variantName, languageCode) {
78
+ const environmentConfig = config.environments[environmentName];
79
+ const variantConfig = variantName ? config.variants[variantName] : null;
80
+ return {
81
+ env: environmentName,
82
+ stackPrefix: environmentConfig.stackPrefix,
83
+ project: config.project.name,
84
+ variant: variantName ?? "website",
85
+ lang: languageCode ?? variantConfig?.defaultLanguage ?? "en"
86
+ };
87
+ }
88
+
89
+ export async function loadProjectConfig(configPath) {
90
+ const raw = await fs.readFile(configPath, "utf8");
91
+ try {
92
+ return JSON.parse(raw);
93
+ } catch (error) {
94
+ throw new S3teError("CONFIG_SCHEMA_ERROR", "Config file is not valid JSON.", { configPath, cause: error.message });
95
+ }
96
+ }
97
+
98
+ export function resolveProjectConfig(projectConfig) {
99
+ const configVersion = projectConfig.configVersion ?? 1;
100
+ const project = {
101
+ name: projectConfig.project?.name,
102
+ displayName: projectConfig.project?.displayName
103
+ };
104
+
105
+ const rendering = {
106
+ minifyHtml: projectConfig.rendering?.minifyHtml ?? true,
107
+ renderExtensions: projectConfig.rendering?.renderExtensions ?? [".html", ".htm", ".part"],
108
+ outputDir: projectConfig.rendering?.outputDir ?? "offline/S3TELocal/preview",
109
+ maxRenderDepth: projectConfig.rendering?.maxRenderDepth ?? 50
110
+ };
111
+
112
+ const environments = {};
113
+ for (const [environmentName, environmentConfig] of Object.entries(projectConfig.environments ?? {})) {
114
+ environments[environmentName] = {
115
+ name: environmentName,
116
+ awsRegion: environmentConfig.awsRegion,
117
+ stackPrefix: environmentConfig.stackPrefix ?? upperSnakeCase(environmentName),
118
+ certificateArn: environmentConfig.certificateArn,
119
+ route53HostedZoneId: environmentConfig.route53HostedZoneId
120
+ };
121
+ }
122
+
123
+ const variants = {};
124
+ const awsCodeBuckets = { ...(projectConfig.aws?.codeBuckets ?? {}) };
125
+
126
+ for (const [variantName, variantConfig] of Object.entries(projectConfig.variants ?? {})) {
127
+ const languages = {};
128
+ const languageEntries = Object.entries(variantConfig.languages ?? {});
129
+ for (const [languageCode, languageConfig] of languageEntries) {
130
+ languages[languageCode] = {
131
+ code: languageCode,
132
+ baseUrl: languageConfig.baseUrl,
133
+ targetBucket: languageConfig.targetBucket,
134
+ cloudFrontAliases: [...(languageConfig.cloudFrontAliases ?? [])],
135
+ webinyLocale: languageConfig.webinyLocale ?? languageCode
136
+ };
137
+ }
138
+
139
+ variants[variantName] = {
140
+ name: variantName,
141
+ sourceDir: normalizeRelativeProjectPath(variantConfig.sourceDir ?? `app/${variantName}`),
142
+ partDir: normalizeRelativeProjectPath(variantConfig.partDir ?? "app/part"),
143
+ defaultLanguage: variantConfig.defaultLanguage,
144
+ routing: {
145
+ indexDocument: variantConfig.routing?.indexDocument ?? "index.html",
146
+ notFoundDocument: variantConfig.routing?.notFoundDocument ?? "404.html"
147
+ },
148
+ languages
149
+ };
150
+
151
+ awsCodeBuckets[variantName] = awsCodeBuckets[variantName] ?? "{env}-{variant}-code-{project}";
152
+ }
153
+
154
+ const aws = {
155
+ codeBuckets: awsCodeBuckets,
156
+ dependencyStore: {
157
+ tableName: projectConfig.aws?.dependencyStore?.tableName ?? "{stackPrefix}_s3te_dependencies_{project}"
158
+ },
159
+ contentStore: {
160
+ tableName: projectConfig.aws?.contentStore?.tableName ?? "{stackPrefix}_s3te_content_{project}",
161
+ contentIdIndexName: projectConfig.aws?.contentStore?.contentIdIndexName ?? "contentid"
162
+ },
163
+ invalidationStore: {
164
+ tableName: projectConfig.aws?.invalidationStore?.tableName ?? "{stackPrefix}_s3te_invalidations_{project}",
165
+ debounceSeconds: projectConfig.aws?.invalidationStore?.debounceSeconds ?? 60
166
+ },
167
+ lambda: {
168
+ runtime: projectConfig.aws?.lambda?.runtime ?? "nodejs22.x",
169
+ architecture: projectConfig.aws?.lambda?.architecture ?? "arm64"
170
+ }
171
+ };
172
+
173
+ const integrations = {
174
+ webiny: {
175
+ enabled: projectConfig.integrations?.webiny?.enabled ?? false,
176
+ sourceTableName: projectConfig.integrations?.webiny?.sourceTableName,
177
+ mirrorTableName: projectConfig.integrations?.webiny?.mirrorTableName ?? "{stackPrefix}_s3te_content_{project}",
178
+ relevantModels: projectConfig.integrations?.webiny?.relevantModels ?? ["staticContent", "staticCodeContent"],
179
+ tenant: projectConfig.integrations?.webiny?.tenant
180
+ }
181
+ };
182
+
183
+ for (const [variantName, variantConfig] of Object.entries(variants)) {
184
+ const languageEntries = Object.values(variantConfig.languages);
185
+ const languageCount = languageEntries.length;
186
+ for (const languageConfig of languageEntries) {
187
+ languageConfig.targetBucket = languageConfig.targetBucket ?? defaultTargetBucketPattern({
188
+ variant: variantName,
189
+ language: languageConfig.code,
190
+ languageCount,
191
+ isDefaultLanguage: languageConfig.code === variantConfig.defaultLanguage,
192
+ project: project.name
193
+ });
194
+ }
195
+ }
196
+
197
+ return {
198
+ configVersion,
199
+ project,
200
+ environments,
201
+ rendering,
202
+ variants,
203
+ aws,
204
+ integrations
205
+ };
206
+ }
207
+
208
+ export function resolveCodeBucketName(config, environmentName, variantName) {
209
+ return replacePlaceholders(
210
+ config.aws.codeBuckets[variantName],
211
+ createPlaceholderContext(config, environmentName, variantName)
212
+ );
213
+ }
214
+
215
+ export function resolveTargetBucketName(config, environmentName, variantName, languageCode) {
216
+ return replacePlaceholders(
217
+ config.variants[variantName].languages[languageCode].targetBucket,
218
+ createPlaceholderContext(config, environmentName, variantName, languageCode)
219
+ );
220
+ }
221
+
222
+ export function resolveTableNames(config, environmentName) {
223
+ const context = createPlaceholderContext(config, environmentName);
224
+ return {
225
+ dependency: replacePlaceholders(config.aws.dependencyStore.tableName, context),
226
+ content: replacePlaceholders(config.aws.contentStore.tableName, context),
227
+ invalidation: replacePlaceholders(config.aws.invalidationStore.tableName, context),
228
+ webinyMirror: replacePlaceholders(config.integrations.webiny.mirrorTableName, context)
229
+ };
230
+ }
231
+
232
+ export function resolveRuntimeManifestParameterName(config, environmentName) {
233
+ const environmentConfig = config.environments[environmentName];
234
+ return `/${environmentConfig.stackPrefix}/s3te/${config.project.name}/runtime-manifest`;
235
+ }
236
+
237
+ export function resolveStackName(config, environmentName) {
238
+ return `${config.environments[environmentName].stackPrefix}-s3te-${config.project.name}`;
239
+ }
240
+
241
+ export function buildEnvironmentRuntimeConfig(config, environmentName, stackOutputs = {}) {
242
+ const environmentConfig = config.environments[environmentName];
243
+ const tables = resolveTableNames(config, environmentName);
244
+ const runtimeParameterName = resolveRuntimeManifestParameterName(config, environmentName);
245
+ const stackName = resolveStackName(config, environmentName);
246
+ const variants = {};
247
+
248
+ for (const [variantName, variantConfig] of Object.entries(config.variants)) {
249
+ const languages = {};
250
+ for (const [languageCode, languageConfig] of Object.entries(variantConfig.languages)) {
251
+ const targetBucket = resolveTargetBucketName(config, environmentName, variantName, languageCode);
252
+ languages[languageCode] = {
253
+ code: languageCode,
254
+ baseUrl: languageConfig.baseUrl,
255
+ targetBucket,
256
+ cloudFrontAliases: [...languageConfig.cloudFrontAliases],
257
+ webinyLocale: languageConfig.webinyLocale,
258
+ distributionId: stackOutputs.distributionIds?.[variantName]?.[languageCode] ?? "",
259
+ distributionDomainName: stackOutputs.distributionDomains?.[variantName]?.[languageCode] ?? ""
260
+ };
261
+ }
262
+
263
+ variants[variantName] = {
264
+ name: variantName,
265
+ sourceDir: variantConfig.sourceDir,
266
+ partDir: variantConfig.partDir,
267
+ defaultLanguage: variantConfig.defaultLanguage,
268
+ routing: { ...variantConfig.routing },
269
+ codeBucket: resolveCodeBucketName(config, environmentName, variantName),
270
+ languages
271
+ };
272
+ }
273
+
274
+ return {
275
+ name: environmentName,
276
+ awsRegion: environmentConfig.awsRegion,
277
+ stackPrefix: environmentConfig.stackPrefix,
278
+ certificateArn: environmentConfig.certificateArn,
279
+ route53HostedZoneId: environmentConfig.route53HostedZoneId,
280
+ stackName,
281
+ runtimeParameterName,
282
+ tables,
283
+ lambda: { ...config.aws.lambda },
284
+ rendering: { ...config.rendering },
285
+ integrations: {
286
+ webiny: {
287
+ ...config.integrations.webiny,
288
+ mirrorTableName: tables.webinyMirror
289
+ }
290
+ },
291
+ variants
292
+ };
293
+ }
294
+
295
+ export async function validateAndResolveProjectConfig(projectConfig, options = {}) {
296
+ const projectDir = options.projectDir ?? process.cwd();
297
+ const errors = [];
298
+ const warnings = [];
299
+
300
+ if (!projectConfig || typeof projectConfig !== "object") {
301
+ throw new S3teError("CONFIG_SCHEMA_ERROR", "Project config must be an object.");
302
+ }
303
+
304
+ if (!projectConfig.project || typeof projectConfig.project !== "object") {
305
+ errors.push({ code: "CONFIG_SCHEMA_ERROR", message: "Missing project block." });
306
+ } else if (!isValidProjectName(projectConfig.project.name ?? "")) {
307
+ errors.push({
308
+ code: "CONFIG_SCHEMA_ERROR",
309
+ message: "project.name must match ^[a-z0-9-]+$.",
310
+ details: { value: projectConfig.project.name }
311
+ });
312
+ }
313
+
314
+ const environmentEntries = Object.entries(projectConfig.environments ?? {});
315
+ if (environmentEntries.length === 0) {
316
+ errors.push({ code: "CONFIG_SCHEMA_ERROR", message: "At least one environment is required." });
317
+ }
318
+
319
+ for (const [environmentName, environmentConfig] of environmentEntries) {
320
+ if (!environmentConfig.awsRegion) {
321
+ errors.push({ code: "CONFIG_SCHEMA_ERROR", message: `Environment ${environmentName} is missing awsRegion.` });
322
+ }
323
+ if (!environmentConfig.certificateArn) {
324
+ errors.push({ code: "CONFIG_SCHEMA_ERROR", message: `Environment ${environmentName} is missing certificateArn.` });
325
+ } else if (!/^arn:aws:acm:us-east-1:\d{12}:certificate\/.+$/.test(environmentConfig.certificateArn)) {
326
+ errors.push({
327
+ code: "CONFIG_CONFLICT_ERROR",
328
+ message: `Environment ${environmentName} certificateArn must point to ACM in us-east-1.`,
329
+ details: { certificateArn: environmentConfig.certificateArn }
330
+ });
331
+ }
332
+ if (environmentConfig.stackPrefix && !isValidUpperSnake(environmentConfig.stackPrefix)) {
333
+ errors.push({
334
+ code: "CONFIG_SCHEMA_ERROR",
335
+ message: `Environment ${environmentName} stackPrefix must match ^[A-Z0-9_]+$.`,
336
+ details: { stackPrefix: environmentConfig.stackPrefix }
337
+ });
338
+ }
339
+ }
340
+
341
+ const variantEntries = Object.entries(projectConfig.variants ?? {});
342
+ if (variantEntries.length === 0) {
343
+ errors.push({ code: "CONFIG_SCHEMA_ERROR", message: "At least one variant is required." });
344
+ }
345
+
346
+ for (const [variantName, variantConfig] of variantEntries) {
347
+ const languageEntries = Object.entries(variantConfig.languages ?? {});
348
+ if (languageEntries.length === 0) {
349
+ errors.push({ code: "CONFIG_SCHEMA_ERROR", message: `Variant ${variantName} needs at least one language.` });
350
+ continue;
351
+ }
352
+
353
+ if (!variantConfig.defaultLanguage || !variantConfig.languages?.[variantConfig.defaultLanguage]) {
354
+ errors.push({
355
+ code: "CONFIG_CONFLICT_ERROR",
356
+ message: `Variant ${variantName} defaultLanguage must exist in languages.`
357
+ });
358
+ }
359
+
360
+ for (const [languageCode, languageConfig] of languageEntries) {
361
+ if (!languageConfig.baseUrl) {
362
+ errors.push({
363
+ code: "CONFIG_SCHEMA_ERROR",
364
+ message: `Variant ${variantName} language ${languageCode} is missing baseUrl.`
365
+ });
366
+ }
367
+ if (!Array.isArray(languageConfig.cloudFrontAliases) || languageConfig.cloudFrontAliases.length === 0) {
368
+ errors.push({
369
+ code: "CONFIG_SCHEMA_ERROR",
370
+ message: `Variant ${variantName} language ${languageCode} needs at least one cloudFrontAlias.`
371
+ });
372
+ }
373
+ if (languageConfig.webinyLocale !== undefined && typeof languageConfig.webinyLocale !== "string") {
374
+ errors.push({
375
+ code: "CONFIG_SCHEMA_ERROR",
376
+ message: `Variant ${variantName} language ${languageCode} webinyLocale must be a string.`,
377
+ details: { value: languageConfig.webinyLocale }
378
+ });
379
+ }
380
+ if (languageConfig.targetBucket) {
381
+ ensureKnownPlaceholders(languageConfig.targetBucket, `variants.${variantName}.languages.${languageCode}.targetBucket`, errors);
382
+ }
383
+ }
384
+ }
385
+
386
+ if (projectConfig.integrations?.webiny?.enabled && !projectConfig.integrations?.webiny?.sourceTableName) {
387
+ errors.push({
388
+ code: "CONFIG_CONFLICT_ERROR",
389
+ message: "Webiny integration requires sourceTableName when enabled."
390
+ });
391
+ }
392
+
393
+ for (const [variantName, pattern] of Object.entries(projectConfig.aws?.codeBuckets ?? {})) {
394
+ ensureKnownPlaceholders(pattern, `aws.codeBuckets.${variantName}`, errors);
395
+ }
396
+
397
+ if (projectConfig.aws?.dependencyStore?.tableName) {
398
+ ensureKnownPlaceholders(projectConfig.aws.dependencyStore.tableName, "aws.dependencyStore.tableName", errors);
399
+ }
400
+ if (projectConfig.aws?.contentStore?.tableName) {
401
+ ensureKnownPlaceholders(projectConfig.aws.contentStore.tableName, "aws.contentStore.tableName", errors);
402
+ }
403
+ if (projectConfig.aws?.invalidationStore?.tableName) {
404
+ ensureKnownPlaceholders(projectConfig.aws.invalidationStore.tableName, "aws.invalidationStore.tableName", errors);
405
+ }
406
+ if (projectConfig.integrations?.webiny?.mirrorTableName) {
407
+ ensureKnownPlaceholders(projectConfig.integrations.webiny.mirrorTableName, "integrations.webiny.mirrorTableName", errors);
408
+ }
409
+
410
+ if (errors.length > 0) {
411
+ return { ok: false, errors, warnings, config: null };
412
+ }
413
+
414
+ const resolvedConfig = resolveProjectConfig(projectConfig);
415
+ const seenTargetBuckets = new Set();
416
+ const seenCodeBuckets = new Set();
417
+
418
+ for (const variantConfig of Object.values(resolvedConfig.variants)) {
419
+ await ensureDirectoryExists(projectDir, variantConfig.sourceDir, errors);
420
+ await ensureDirectoryExists(projectDir, variantConfig.partDir, errors);
421
+ }
422
+
423
+ for (const environmentName of Object.keys(resolvedConfig.environments)) {
424
+ const tables = resolveTableNames(resolvedConfig, environmentName);
425
+
426
+ const tableNames = [tables.dependency, tables.content, tables.invalidation];
427
+ for (const tableName of tableNames) {
428
+ if (!/^[A-Za-z0-9_.-]+$/.test(tableName)) {
429
+ errors.push({
430
+ code: "CONFIG_CONFLICT_ERROR",
431
+ message: `Resolved table name ${tableName} contains invalid characters.`
432
+ });
433
+ }
434
+ }
435
+
436
+ for (const [variantName, variantConfig] of Object.entries(resolvedConfig.variants)) {
437
+ const codeBucket = resolveCodeBucketName(resolvedConfig, environmentName, variantName);
438
+ if (seenCodeBuckets.has(codeBucket)) {
439
+ errors.push({
440
+ code: "CONFIG_CONFLICT_ERROR",
441
+ message: `Duplicate code bucket ${codeBucket}.`
442
+ });
443
+ }
444
+ seenCodeBuckets.add(codeBucket);
445
+
446
+ for (const languageCode of Object.keys(variantConfig.languages)) {
447
+ const targetBucket = resolveTargetBucketName(resolvedConfig, environmentName, variantName, languageCode);
448
+ if (seenTargetBuckets.has(targetBucket)) {
449
+ errors.push({
450
+ code: "CONFIG_CONFLICT_ERROR",
451
+ message: `Duplicate target bucket ${targetBucket}.`
452
+ });
453
+ }
454
+ seenTargetBuckets.add(targetBucket);
455
+ }
456
+ }
457
+ }
458
+
459
+ if (errors.length > 0) {
460
+ return { ok: false, errors, warnings, config: null };
461
+ }
462
+
463
+ return { ok: true, errors, warnings, config: resolvedConfig };
464
+ }
@@ -0,0 +1,176 @@
1
+ function legacyAttributeValueToPlain(attribute) {
2
+ if (attribute == null || typeof attribute !== "object") {
3
+ return undefined;
4
+ }
5
+
6
+ if ("S" in attribute) {
7
+ return attribute.S;
8
+ }
9
+ if ("N" in attribute) {
10
+ return Number(attribute.N);
11
+ }
12
+ if ("BOOL" in attribute) {
13
+ return Boolean(attribute.BOOL);
14
+ }
15
+ if ("NULL" in attribute) {
16
+ return null;
17
+ }
18
+ if ("L" in attribute) {
19
+ return attribute.L.map((entry) => legacyAttributeValueToPlain(entry));
20
+ }
21
+
22
+ return undefined;
23
+ }
24
+
25
+ function readComparableValue(item, field) {
26
+ if (field === "__typename") {
27
+ return item.model;
28
+ }
29
+ if (field === "contentId") {
30
+ return item.contentId;
31
+ }
32
+ if (field === "id") {
33
+ return item.id;
34
+ }
35
+ if (field === "locale") {
36
+ return item.locale ?? null;
37
+ }
38
+ if (field === "tenant") {
39
+ return item.tenant ?? null;
40
+ }
41
+ if (field === "_version") {
42
+ return item.version ?? null;
43
+ }
44
+ if (field === "_lastChangedAt") {
45
+ return item.lastChangedAt ?? null;
46
+ }
47
+
48
+ return item.values?.[field];
49
+ }
50
+
51
+ function compareFilter(actual, expected, filterType) {
52
+ if (filterType === "contains") {
53
+ if (Array.isArray(actual)) {
54
+ return actual.includes(expected);
55
+ }
56
+
57
+ if (typeof actual === "string" && typeof expected === "string") {
58
+ return actual.includes(expected);
59
+ }
60
+ }
61
+
62
+ if (Array.isArray(actual) && Array.isArray(expected)) {
63
+ return JSON.stringify(actual) === JSON.stringify(expected);
64
+ }
65
+
66
+ return actual === expected;
67
+ }
68
+
69
+ function compareOrder(a, b) {
70
+ const aHasOrder = typeof a.values?.order === "number";
71
+ const bHasOrder = typeof b.values?.order === "number";
72
+
73
+ if (aHasOrder && bHasOrder) {
74
+ if (a.values.order !== b.values.order) {
75
+ return a.values.order - b.values.order;
76
+ }
77
+ } else if (aHasOrder && !bHasOrder) {
78
+ return -1;
79
+ } else if (!aHasOrder && bHasOrder) {
80
+ return 1;
81
+ }
82
+
83
+ if (a.contentId !== b.contentId) {
84
+ return String(a.contentId).localeCompare(String(b.contentId));
85
+ }
86
+
87
+ return String(a.id).localeCompare(String(b.id));
88
+ }
89
+
90
+ export function applyContentQuery(items, query) {
91
+ const filterType = query.filterType ?? "equals";
92
+ const clauses = query.filter ?? [];
93
+
94
+ let results = items.filter((item) => {
95
+ for (const clause of clauses) {
96
+ const entries = Object.entries(clause);
97
+ if (entries.length !== 1) {
98
+ return false;
99
+ }
100
+
101
+ const [field, legacyValue] = entries[0];
102
+ const expected = legacyAttributeValueToPlain(legacyValue);
103
+ const actual = readComparableValue(item, field);
104
+ if (!compareFilter(actual, expected, filterType)) {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ return true;
110
+ });
111
+
112
+ results = results.sort(compareOrder);
113
+
114
+ if (typeof query.limit === "number") {
115
+ if (query.limit <= 0) {
116
+ return [];
117
+ }
118
+ return results.slice(0, query.limit);
119
+ }
120
+
121
+ return results;
122
+ }
123
+
124
+ export function readContentField(item, field, language) {
125
+ if (!item) {
126
+ return null;
127
+ }
128
+
129
+ if (field === "__typename") {
130
+ return item.model;
131
+ }
132
+ if (field === "contentId") {
133
+ return item.contentId;
134
+ }
135
+ if (field === "id") {
136
+ return item.id;
137
+ }
138
+ if (field === "locale") {
139
+ return item.locale ?? null;
140
+ }
141
+ if (field === "tenant") {
142
+ return item.tenant ?? null;
143
+ }
144
+ if (field === "_version") {
145
+ return item.version ?? null;
146
+ }
147
+ if (field === "_lastChangedAt") {
148
+ return item.lastChangedAt ?? null;
149
+ }
150
+
151
+ if (field === "content") {
152
+ const preferred = item.values?.[`content${language}`];
153
+ if (preferred !== undefined) {
154
+ return preferred;
155
+ }
156
+ }
157
+
158
+ return item.values?.[field] ?? null;
159
+ }
160
+
161
+ export function serializeContentValue(value) {
162
+ if (value == null) {
163
+ return "";
164
+ }
165
+
166
+ if (Array.isArray(value)) {
167
+ return value.map((entry) => {
168
+ const text = typeof entry === "string" && entry.includes("-")
169
+ ? entry.slice(entry.lastIndexOf("-") + 1)
170
+ : String(entry);
171
+ return `<a href='${String(entry)}'>${text}</a>`;
172
+ }).join("");
173
+ }
174
+
175
+ return String(value);
176
+ }
@@ -0,0 +1,14 @@
1
+ export class S3teError extends Error {
2
+ constructor(code, message, details = undefined) {
3
+ super(message);
4
+ this.name = "S3teError";
5
+ this.code = code;
6
+ this.details = details;
7
+ }
8
+ }
9
+
10
+ export function assert(condition, code, message, details = undefined) {
11
+ if (!condition) {
12
+ throw new S3teError(code, message, details);
13
+ }
14
+ }
@@ -0,0 +1,24 @@
1
+ export { S3teError } from "./errors.mjs";
2
+ export { getContentTypeForPath } from "./mime.mjs";
3
+ export { minifyHtml, repairTruncatedHtml } from "./minify.mjs";
4
+ export {
5
+ buildEnvironmentRuntimeConfig,
6
+ loadProjectConfig,
7
+ resolveCodeBucketName,
8
+ resolveRuntimeManifestParameterName,
9
+ resolveProjectConfig,
10
+ resolveStackName,
11
+ resolveTableNames,
12
+ resolveTargetBucketName,
13
+ validateAndResolveProjectConfig
14
+ } from "./config.mjs";
15
+ export {
16
+ applyContentQuery,
17
+ readContentField,
18
+ serializeContentValue
19
+ } from "./content-query.mjs";
20
+ export {
21
+ createManualRenderTargets,
22
+ isRenderableKey,
23
+ renderSourceTemplate
24
+ } from "./render.mjs";