@ampless/backend 1.0.0-alpha.26 → 1.0.0-alpha.28
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/dist/events/dispatcher.js +30 -3
- package/dist/events/processor-trusted.js +69 -1
- package/dist/functions/mcp-handler.js +5 -102
- package/dist/index.js +12 -7
- package/package.json +3 -3
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
SQSClient,
|
|
7
7
|
SendMessageBatchCommand
|
|
8
8
|
} from "@aws-sdk/client-sqs";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
detectContentEvents
|
|
11
|
+
} from "ampless";
|
|
10
12
|
function requireEnv(name) {
|
|
11
13
|
const v = process.env[name];
|
|
12
14
|
if (!v) throw new Error(`event-dispatcher: missing required env var ${name}`);
|
|
@@ -20,15 +22,37 @@ function tableNameFromArn(arn) {
|
|
|
20
22
|
const match = arn.match(/:table\/([^/]+)/);
|
|
21
23
|
return match ? match[1] : null;
|
|
22
24
|
}
|
|
25
|
+
function projectPost(raw) {
|
|
26
|
+
if (!raw || !raw.postId || !raw.slug || !raw.title) return null;
|
|
27
|
+
return {
|
|
28
|
+
postId: raw.postId,
|
|
29
|
+
slug: raw.slug,
|
|
30
|
+
title: raw.title,
|
|
31
|
+
status: raw.status ?? "draft",
|
|
32
|
+
publishedAt: raw.publishedAt,
|
|
33
|
+
tags: raw.tags
|
|
34
|
+
};
|
|
35
|
+
}
|
|
23
36
|
function emitContentEvents(record, timestamp) {
|
|
24
37
|
const oldItem = record.dynamodb?.OldImage ? unmarshall(record.dynamodb.OldImage) : null;
|
|
25
38
|
const newItem = record.dynamodb?.NewImage ? unmarshall(record.dynamodb.NewImage) : null;
|
|
39
|
+
const events = [];
|
|
40
|
+
const previous = oldItem ? projectPost(oldItem) : null;
|
|
41
|
+
const next = newItem ? projectPost(newItem) : null;
|
|
42
|
+
if (previous || next) {
|
|
43
|
+
const indexPayload = { previous, next };
|
|
44
|
+
events.push({
|
|
45
|
+
type: "post.index.refresh",
|
|
46
|
+
payload: indexPayload,
|
|
47
|
+
timestamp
|
|
48
|
+
});
|
|
49
|
+
}
|
|
26
50
|
const types = detectContentEvents({
|
|
27
51
|
eventName: record.eventName,
|
|
28
52
|
oldStatus: oldItem?.status,
|
|
29
53
|
newStatus: newItem?.status
|
|
30
54
|
});
|
|
31
|
-
if (types.length === 0) return
|
|
55
|
+
if (types.length === 0) return events;
|
|
32
56
|
const item = newItem ?? oldItem ?? {};
|
|
33
57
|
const payload = {
|
|
34
58
|
postId: item.postId,
|
|
@@ -38,7 +62,10 @@ function emitContentEvents(record, timestamp) {
|
|
|
38
62
|
publishedAt: item.publishedAt,
|
|
39
63
|
tags: item.tags
|
|
40
64
|
};
|
|
41
|
-
|
|
65
|
+
for (const type of types) {
|
|
66
|
+
events.push({ type, payload, timestamp });
|
|
67
|
+
}
|
|
68
|
+
return events;
|
|
42
69
|
}
|
|
43
70
|
function emitKvEvents(record, timestamp) {
|
|
44
71
|
const item = record.dynamodb?.NewImage ?? record.dynamodb?.OldImage ? unmarshall(record.dynamodb.NewImage ?? record.dynamodb.OldImage) : {};
|
|
@@ -3,10 +3,44 @@ import "../chunk-BYXBJQAS.js";
|
|
|
3
3
|
// src/events/processor-trusted.ts
|
|
4
4
|
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
5
5
|
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
DynamoDBDocumentClient,
|
|
8
|
+
DeleteCommand,
|
|
9
|
+
PutCommand,
|
|
10
|
+
QueryCommand
|
|
11
|
+
} from "@aws-sdk/lib-dynamodb";
|
|
7
12
|
import {
|
|
8
13
|
formatPublicAssetUrl
|
|
9
14
|
} from "ampless";
|
|
15
|
+
|
|
16
|
+
// src/events/posttag-sync.ts
|
|
17
|
+
function computePostTagDiff(payload) {
|
|
18
|
+
const previousEntries = postTagItemsFromPost(payload.previous);
|
|
19
|
+
const nextEntries = postTagItemsFromPost(payload.next);
|
|
20
|
+
const nextKeys = new Set(nextEntries.map(itemKey));
|
|
21
|
+
const deletes = previousEntries.filter((p) => !nextKeys.has(itemKey(p))).map((p) => ({ tag: p.tag, publishedAtPostId: p.publishedAtPostId }));
|
|
22
|
+
return { deletes, puts: nextEntries };
|
|
23
|
+
}
|
|
24
|
+
function postTagItemsFromPost(p) {
|
|
25
|
+
if (!p) return [];
|
|
26
|
+
if (p.status !== "published") return [];
|
|
27
|
+
if (!p.publishedAt) return [];
|
|
28
|
+
const tags = Array.isArray(p.tags) ? p.tags : [];
|
|
29
|
+
return tags.filter((t) => typeof t === "string" && t.length > 0).map((tag) => ({
|
|
30
|
+
tag,
|
|
31
|
+
publishedAtPostId: `${p.publishedAt}#${p.postId}`,
|
|
32
|
+
postId: p.postId,
|
|
33
|
+
publishedAt: p.publishedAt,
|
|
34
|
+
slug: p.slug,
|
|
35
|
+
title: p.title,
|
|
36
|
+
tags
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
function itemKey(item) {
|
|
40
|
+
return `${item.tag}|${item.publishedAtPostId}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/events/processor-trusted.ts
|
|
10
44
|
function requireEnv(name) {
|
|
11
45
|
const v = process.env[name];
|
|
12
46
|
if (!v) throw new Error(`processor-trusted: missing required env var ${name}`);
|
|
@@ -30,6 +64,7 @@ function createProcessorTrustedHandler(opts) {
|
|
|
30
64
|
const BUCKET = requireEnv("AMPLESS_BUCKET_NAME");
|
|
31
65
|
const POST_TABLE = requireEnv("AMPLESS_POST_TABLE");
|
|
32
66
|
const KV_TABLE = requireEnv("AMPLESS_KV_TABLE");
|
|
67
|
+
const POSTTAG_TABLE = requireEnv("AMPLESS_POSTTAG_TABLE");
|
|
33
68
|
const REGION = requireEnv("AWS_REGION");
|
|
34
69
|
async function listPublished() {
|
|
35
70
|
const items = [];
|
|
@@ -82,6 +117,31 @@ function createProcessorTrustedHandler(opts) {
|
|
|
82
117
|
}
|
|
83
118
|
};
|
|
84
119
|
}
|
|
120
|
+
async function rebuildPostTagsForPost(payload) {
|
|
121
|
+
const { deletes, puts } = computePostTagDiff(payload);
|
|
122
|
+
await Promise.all([
|
|
123
|
+
...deletes.map(
|
|
124
|
+
(key) => ddb.send(
|
|
125
|
+
new DeleteCommand({
|
|
126
|
+
TableName: POSTTAG_TABLE,
|
|
127
|
+
Key: key
|
|
128
|
+
})
|
|
129
|
+
)
|
|
130
|
+
),
|
|
131
|
+
...puts.map(
|
|
132
|
+
(item) => ddb.send(
|
|
133
|
+
new PutCommand({
|
|
134
|
+
TableName: POSTTAG_TABLE,
|
|
135
|
+
Item: item
|
|
136
|
+
})
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
]);
|
|
140
|
+
const postId = payload.next?.postId ?? payload.previous?.postId ?? "(unknown)";
|
|
141
|
+
console.log(
|
|
142
|
+
`[posttag-sync] postId=${postId} removed=${deletes.length} upserted=${puts.length}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
85
145
|
async function rebuildSiteSettingsCache() {
|
|
86
146
|
const settings = {};
|
|
87
147
|
let exclusiveStartKey;
|
|
@@ -136,6 +196,14 @@ function createProcessorTrustedHandler(opts) {
|
|
|
136
196
|
throw err;
|
|
137
197
|
}
|
|
138
198
|
}
|
|
199
|
+
if (parsed.type === "post.index.refresh") {
|
|
200
|
+
try {
|
|
201
|
+
await rebuildPostTagsForPost(parsed.payload);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.error("[trusted-processor] posttag-sync failed", err);
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
139
207
|
for (const plugin of trustedPlugins) {
|
|
140
208
|
const hook = plugin.hooks?.[parsed.type];
|
|
141
209
|
if (!hook) continue;
|
|
@@ -5,7 +5,7 @@ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
|
5
5
|
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
|
|
6
6
|
import { createHash } from "crypto";
|
|
7
7
|
|
|
8
|
-
// ../mcp-server/dist/chunk-
|
|
8
|
+
// ../mcp-server/dist/chunk-WAP4WAMQ.js
|
|
9
9
|
import {
|
|
10
10
|
decodeAwsJson
|
|
11
11
|
} from "ampless";
|
|
@@ -159,92 +159,6 @@ async function getPost(client, args) {
|
|
|
159
159
|
const item = data.listPosts.items[0];
|
|
160
160
|
return item ? toCorePost(item) : null;
|
|
161
161
|
}
|
|
162
|
-
function entries(post) {
|
|
163
|
-
if (post.status !== "published" || !post.publishedAt || !post.tags?.length) return [];
|
|
164
|
-
return post.tags.map((tag) => ({
|
|
165
|
-
tag,
|
|
166
|
-
publishedAtPostId: `${post.publishedAt}#${post.postId}`
|
|
167
|
-
}));
|
|
168
|
-
}
|
|
169
|
-
function entryKey(e) {
|
|
170
|
-
return `${e.tag}|${e.publishedAtPostId}`;
|
|
171
|
-
}
|
|
172
|
-
var CREATE_POST_TAG = (
|
|
173
|
-
/* GraphQL */
|
|
174
|
-
`
|
|
175
|
-
mutation CreatePostTag($input: CreatePostTagInput!) {
|
|
176
|
-
createPostTag(input: $input) {
|
|
177
|
-
tag
|
|
178
|
-
publishedAtPostId
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
`
|
|
182
|
-
);
|
|
183
|
-
var UPDATE_POST_TAG = (
|
|
184
|
-
/* GraphQL */
|
|
185
|
-
`
|
|
186
|
-
mutation UpdatePostTag($input: UpdatePostTagInput!) {
|
|
187
|
-
updatePostTag(input: $input) {
|
|
188
|
-
tag
|
|
189
|
-
publishedAtPostId
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
`
|
|
193
|
-
);
|
|
194
|
-
var DELETE_POST_TAG = (
|
|
195
|
-
/* GraphQL */
|
|
196
|
-
`
|
|
197
|
-
mutation DeletePostTag($input: DeletePostTagInput!) {
|
|
198
|
-
deletePostTag(input: $input) {
|
|
199
|
-
tag
|
|
200
|
-
publishedAtPostId
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
`
|
|
204
|
-
);
|
|
205
|
-
async function syncPostTags(client, post, oldPost) {
|
|
206
|
-
const oldEntries = oldPost ? entries(oldPost) : [];
|
|
207
|
-
const newEntries = entries(post);
|
|
208
|
-
const oldKeys = new Set(oldEntries.map(entryKey));
|
|
209
|
-
const newKeys = new Set(newEntries.map(entryKey));
|
|
210
|
-
await Promise.all(
|
|
211
|
-
oldEntries.filter((e) => !newKeys.has(entryKey(e))).map(
|
|
212
|
-
(e) => client.query(DELETE_POST_TAG, {
|
|
213
|
-
input: { tag: e.tag, publishedAtPostId: e.publishedAtPostId }
|
|
214
|
-
})
|
|
215
|
-
)
|
|
216
|
-
);
|
|
217
|
-
await Promise.all(
|
|
218
|
-
newEntries.filter((e) => !oldKeys.has(entryKey(e))).map(
|
|
219
|
-
(e) => client.query(CREATE_POST_TAG, {
|
|
220
|
-
input: {
|
|
221
|
-
tag: e.tag,
|
|
222
|
-
publishedAtPostId: e.publishedAtPostId,
|
|
223
|
-
postId: post.postId,
|
|
224
|
-
publishedAt: post.publishedAt,
|
|
225
|
-
slug: post.slug,
|
|
226
|
-
title: post.title,
|
|
227
|
-
excerpt: post.excerpt,
|
|
228
|
-
tags: post.tags ?? []
|
|
229
|
-
}
|
|
230
|
-
})
|
|
231
|
-
)
|
|
232
|
-
);
|
|
233
|
-
await Promise.all(
|
|
234
|
-
newEntries.filter((e) => oldKeys.has(entryKey(e))).map(
|
|
235
|
-
(e) => client.query(UPDATE_POST_TAG, {
|
|
236
|
-
input: {
|
|
237
|
-
tag: e.tag,
|
|
238
|
-
publishedAtPostId: e.publishedAtPostId,
|
|
239
|
-
slug: post.slug,
|
|
240
|
-
title: post.title,
|
|
241
|
-
excerpt: post.excerpt,
|
|
242
|
-
tags: post.tags ?? []
|
|
243
|
-
}
|
|
244
|
-
})
|
|
245
|
-
)
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
162
|
var MUTATION = (
|
|
249
163
|
/* GraphQL */
|
|
250
164
|
`
|
|
@@ -312,9 +226,7 @@ async function createPost(client, args) {
|
|
|
312
226
|
metadata: args.metadata !== void 0 ? encodeAwsJson(args.metadata) : void 0
|
|
313
227
|
}
|
|
314
228
|
});
|
|
315
|
-
|
|
316
|
-
await syncPostTags(client, created, null);
|
|
317
|
-
return created;
|
|
229
|
+
return toCorePost(data.createPost);
|
|
318
230
|
}
|
|
319
231
|
var MUTATION2 = (
|
|
320
232
|
/* GraphQL */
|
|
@@ -364,7 +276,6 @@ var updatePostSchema = {
|
|
|
364
276
|
}
|
|
365
277
|
};
|
|
366
278
|
async function updatePost(client, args) {
|
|
367
|
-
const oldPost = await getPost(client, { postId: args.postId });
|
|
368
279
|
const input = { postId: args.postId };
|
|
369
280
|
if (args.slug !== void 0) input.slug = args.slug;
|
|
370
281
|
if (args.title !== void 0) input.title = args.title;
|
|
@@ -376,9 +287,7 @@ async function updatePost(client, args) {
|
|
|
376
287
|
if (args.tags !== void 0) input.tags = args.tags;
|
|
377
288
|
if (args.metadata !== void 0) input.metadata = encodeAwsJson2(args.metadata);
|
|
378
289
|
const data = await client.query(MUTATION2, { input });
|
|
379
|
-
|
|
380
|
-
await syncPostTags(client, updated, oldPost);
|
|
381
|
-
return updated;
|
|
290
|
+
return toCorePost(data.updatePost);
|
|
382
291
|
}
|
|
383
292
|
var MUTATION3 = (
|
|
384
293
|
/* GraphQL */
|
|
@@ -398,10 +307,6 @@ var deletePostSchema = {
|
|
|
398
307
|
}
|
|
399
308
|
};
|
|
400
309
|
async function deletePost(client, args) {
|
|
401
|
-
const oldPost = await getPost(client, { postId: args.postId });
|
|
402
|
-
if (oldPost) {
|
|
403
|
-
await syncPostTags(client, { ...oldPost, status: "draft" }, oldPost);
|
|
404
|
-
}
|
|
405
310
|
const data = await client.query(MUTATION3, { input: { postId: args.postId } });
|
|
406
311
|
return { deleted: data.deletePost };
|
|
407
312
|
}
|
|
@@ -526,11 +431,11 @@ function getSchema() {
|
|
|
526
431
|
var DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
527
432
|
function extractZipFromBuffer(buffer, opts = {}) {
|
|
528
433
|
const maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
529
|
-
const
|
|
434
|
+
const entries = unzipSync(buffer);
|
|
530
435
|
const files = [];
|
|
531
436
|
const issues = [];
|
|
532
437
|
let totalBytes = 0;
|
|
533
|
-
for (const [name, data] of Object.entries(
|
|
438
|
+
for (const [name, data] of Object.entries(entries)) {
|
|
534
439
|
if (name.endsWith("/")) continue;
|
|
535
440
|
const reason = validateBundlePath(name);
|
|
536
441
|
if (reason) {
|
|
@@ -588,7 +493,6 @@ async function upsertStaticPost(graphql, slug, body, fields) {
|
|
|
588
493
|
}
|
|
589
494
|
const data2 = await graphql.query(UPDATE_MUTATION, { input: input2 });
|
|
590
495
|
const updated = toCorePost(data2.updatePost);
|
|
591
|
-
await syncPostTags(graphql, updated, existing);
|
|
592
496
|
return { post: updated, created: false };
|
|
593
497
|
}
|
|
594
498
|
if (!fields.title) {
|
|
@@ -613,7 +517,6 @@ async function upsertStaticPost(graphql, slug, body, fields) {
|
|
|
613
517
|
if (fields.metadata !== void 0) input.metadata = encodeAwsJson3(fields.metadata);
|
|
614
518
|
const data = await graphql.query(CREATE_MUTATION, { input });
|
|
615
519
|
const created = toCorePost(data.createPost);
|
|
616
|
-
await syncPostTags(graphql, created, null);
|
|
617
520
|
return { post: created, created: true };
|
|
618
521
|
}
|
|
619
522
|
var uploadStaticBundleSchema = {
|
package/dist/index.js
CHANGED
|
@@ -133,25 +133,30 @@ function defineAmplessBackend(opts) {
|
|
|
133
133
|
})
|
|
134
134
|
);
|
|
135
135
|
kvTable.grantReadData(trustedFn);
|
|
136
|
+
const postTagTable = backend.data.resources.tables["PostTag"];
|
|
137
|
+
postTagTable.grantWriteData(trustedFn);
|
|
136
138
|
trustedFn.addToRolePolicy(
|
|
137
139
|
new PolicyStatement({
|
|
138
140
|
effect: Effect.ALLOW,
|
|
139
141
|
actions: ["s3:PutObject", "s3:DeleteObject"],
|
|
140
142
|
resources: [
|
|
141
143
|
`${backend.storage.resources.bucket.bucketArn}/public/plugins/*`,
|
|
142
|
-
// Built-in cache: rebuildSiteSettingsCache writes the
|
|
143
|
-
// JSON the public site reads.
|
|
144
|
-
// `site
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
|
|
144
|
+
// Built-in cache: rebuildSiteSettingsCache writes the single
|
|
145
|
+
// JSON file the public site reads. Exact-match resource —
|
|
146
|
+
// historically this was `public/site-settings/<siteId>.json`
|
|
147
|
+
// (multi-site era) and the wildcard pattern that worked then
|
|
148
|
+
// (`public/site-settings/*`) does NOT match the current
|
|
149
|
+
// single-file key `public/site-settings.json`. Wrong pattern
|
|
150
|
+
// fails the PutObject silently with AccessDenied, so the
|
|
151
|
+
// public site never sees admin-side theme / settings changes.
|
|
152
|
+
`${backend.storage.resources.bucket.bucketArn}/public/site-settings.json`
|
|
149
153
|
]
|
|
150
154
|
})
|
|
151
155
|
);
|
|
152
156
|
trustedFn.addEnvironment("AMPLESS_BUCKET_NAME", backend.storage.resources.bucket.bucketName);
|
|
153
157
|
trustedFn.addEnvironment("AMPLESS_POST_TABLE", postTable.tableName);
|
|
154
158
|
trustedFn.addEnvironment("AMPLESS_KV_TABLE", kvTable.tableName);
|
|
159
|
+
trustedFn.addEnvironment("AMPLESS_POSTTAG_TABLE", postTagTable.tableName);
|
|
155
160
|
const untrustedFn = backend.processorUntrusted.resources.lambda;
|
|
156
161
|
untrustedFn.addEventSource(new SqsEventSource(untrustedQueue, { batchSize: 5 }));
|
|
157
162
|
const graphqlApi = backend.data.resources.cfnResources.cfnGraphqlApi;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/backend",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.28",
|
|
4
4
|
"description": "Amplify Gen 2 backend factories for ampless: auth, data, storage, event processors, API key renewer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"@smithy/protocol-http": "^5.4.4",
|
|
66
66
|
"@smithy/signature-v4": "^5.4.4",
|
|
67
67
|
"fflate": "^0.8.3",
|
|
68
|
-
"ampless": "1.0.0-alpha.
|
|
69
|
-
"@ampless/mcp-server": "1.0.0-alpha.
|
|
68
|
+
"ampless": "1.0.0-alpha.15",
|
|
69
|
+
"@ampless/mcp-server": "1.0.0-alpha.17"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
72
|
"@aws-amplify/backend": "^1",
|