@ampless/backend 1.0.0-alpha.30 → 1.0.0-alpha.31
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/functions/mcp-handler.js +45 -16
- package/dist/index.d.ts +5 -2
- package/dist/index.js +44 -3
- package/package.json +3 -3
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
encodeAwsJson as encodeAwsJson2
|
|
15
15
|
} from "ampless";
|
|
16
|
+
import { encodeAwsJson as encodeAwsJson3 } from "ampless";
|
|
16
17
|
import {
|
|
17
18
|
bundlePrefix,
|
|
18
19
|
mimeTypeFor,
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
stripCommonPrefix
|
|
26
27
|
} from "ampless";
|
|
27
28
|
import {
|
|
28
|
-
encodeAwsJson as
|
|
29
|
+
encodeAwsJson as encodeAwsJson4
|
|
29
30
|
} from "ampless";
|
|
30
31
|
import {
|
|
31
32
|
bundlePrefix as bundlePrefix2,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
import { bundlePrefix as bundlePrefix3, validateBundlePath as validateBundlePath3 } from "ampless";
|
|
38
39
|
import {
|
|
39
40
|
bundlePrefix as bundlePrefix4,
|
|
41
|
+
mimeTypeFor as mimeTypeFor3,
|
|
40
42
|
pickDefaultEntrypoint as pickDefaultEntrypoint2
|
|
41
43
|
} from "ampless";
|
|
42
44
|
var POST_FIELDS = (
|
|
@@ -327,6 +329,7 @@ var MUTATION4 = (
|
|
|
327
329
|
mimeType
|
|
328
330
|
size
|
|
329
331
|
delivery
|
|
332
|
+
metadata
|
|
330
333
|
}
|
|
331
334
|
}
|
|
332
335
|
`
|
|
@@ -349,7 +352,9 @@ var uploadMediaSchema = {
|
|
|
349
352
|
async function uploadMedia(graphql, storage, args) {
|
|
350
353
|
const body = Buffer.from(args.base64Data, "base64");
|
|
351
354
|
const key = buildMediaKey(args.filename);
|
|
352
|
-
const
|
|
355
|
+
const putResult = await storage.putObject(key, body, args.mimeType);
|
|
356
|
+
const metadata = {};
|
|
357
|
+
if (putResult.etag) metadata.etag = putResult.etag;
|
|
353
358
|
const mediaId = `media-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
354
359
|
const data = await graphql.query(MUTATION4, {
|
|
355
360
|
input: {
|
|
@@ -357,10 +362,11 @@ async function uploadMedia(graphql, storage, args) {
|
|
|
357
362
|
src: key,
|
|
358
363
|
mimeType: args.mimeType,
|
|
359
364
|
size: body.length,
|
|
360
|
-
delivery: "nextjs"
|
|
365
|
+
delivery: "nextjs",
|
|
366
|
+
metadata: encodeAwsJson3(metadata)
|
|
361
367
|
}
|
|
362
368
|
});
|
|
363
|
-
return { media: data.createMedia, url };
|
|
369
|
+
return { media: data.createMedia, url: putResult.url };
|
|
364
370
|
}
|
|
365
371
|
var getSchemaSchema = {
|
|
366
372
|
type: "object",
|
|
@@ -959,7 +965,7 @@ async function upsertStaticPost(graphql, slug, body, fields) {
|
|
|
959
965
|
const input2 = {
|
|
960
966
|
postId: existing.postId,
|
|
961
967
|
format: "static",
|
|
962
|
-
body:
|
|
968
|
+
body: encodeAwsJson4(body)
|
|
963
969
|
};
|
|
964
970
|
if (fields.title !== void 0) input2.title = fields.title;
|
|
965
971
|
if (fields.excerpt !== void 0) input2.excerpt = fields.excerpt;
|
|
@@ -967,7 +973,7 @@ async function upsertStaticPost(graphql, slug, body, fields) {
|
|
|
967
973
|
if (fields.publishedAt !== void 0) input2.publishedAt = fields.publishedAt;
|
|
968
974
|
if (fields.tags !== void 0) input2.tags = fields.tags;
|
|
969
975
|
if (fields.metadata !== void 0) {
|
|
970
|
-
input2.metadata =
|
|
976
|
+
input2.metadata = encodeAwsJson4(fields.metadata);
|
|
971
977
|
}
|
|
972
978
|
const data2 = await graphql.query(UPDATE_MUTATION, { input: input2 });
|
|
973
979
|
const updated = toCorePost(data2.updatePost);
|
|
@@ -986,13 +992,13 @@ async function upsertStaticPost(graphql, slug, body, fields) {
|
|
|
986
992
|
slug,
|
|
987
993
|
title: fields.title,
|
|
988
994
|
format: "static",
|
|
989
|
-
body:
|
|
995
|
+
body: encodeAwsJson4(body),
|
|
990
996
|
status
|
|
991
997
|
};
|
|
992
998
|
if (fields.excerpt !== void 0) input.excerpt = fields.excerpt;
|
|
993
999
|
if (publishedAt !== void 0) input.publishedAt = publishedAt;
|
|
994
1000
|
if (fields.tags !== void 0) input.tags = fields.tags;
|
|
995
|
-
if (fields.metadata !== void 0) input.metadata =
|
|
1001
|
+
if (fields.metadata !== void 0) input.metadata = encodeAwsJson4(fields.metadata);
|
|
996
1002
|
const data = await graphql.query(CREATE_MUTATION, { input });
|
|
997
1003
|
const created = toCorePost(data.createPost);
|
|
998
1004
|
return { post: created, created: true };
|
|
@@ -1062,8 +1068,11 @@ async function uploadStaticBundle(graphql, storage, args) {
|
|
|
1062
1068
|
await storage.deleteObject(obj.key);
|
|
1063
1069
|
}
|
|
1064
1070
|
let uploadedFiles = 0;
|
|
1071
|
+
const filesMeta = {};
|
|
1065
1072
|
for (const f of files) {
|
|
1066
|
-
|
|
1073
|
+
const mimeType = mimeTypeFor(f.path);
|
|
1074
|
+
await storage.putObject(`${prefix}${f.path}`, f.data, mimeType);
|
|
1075
|
+
filesMeta[f.path] = { size: f.data.byteLength, mimeType };
|
|
1067
1076
|
uploadedFiles += 1;
|
|
1068
1077
|
}
|
|
1069
1078
|
const body = {
|
|
@@ -1071,6 +1080,10 @@ async function uploadStaticBundle(graphql, storage, args) {
|
|
|
1071
1080
|
files: files.map((f) => f.path).sort(),
|
|
1072
1081
|
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1073
1082
|
};
|
|
1083
|
+
const mergedMetadata = {
|
|
1084
|
+
...args.metadata ?? {},
|
|
1085
|
+
files: filesMeta
|
|
1086
|
+
};
|
|
1074
1087
|
const { post } = await upsertStaticPost(graphql, slug, body, {
|
|
1075
1088
|
title: args.title,
|
|
1076
1089
|
postId: args.postId,
|
|
@@ -1078,7 +1091,7 @@ async function uploadStaticBundle(graphql, storage, args) {
|
|
|
1078
1091
|
publishedAt: args.publishedAt,
|
|
1079
1092
|
excerpt: args.excerpt,
|
|
1080
1093
|
tags: args.tags,
|
|
1081
|
-
metadata:
|
|
1094
|
+
metadata: mergedMetadata
|
|
1082
1095
|
});
|
|
1083
1096
|
return { post, bundle: body, uploadedFiles };
|
|
1084
1097
|
}
|
|
@@ -1123,8 +1136,8 @@ async function uploadStaticFile(storage, args) {
|
|
|
1123
1136
|
}
|
|
1124
1137
|
const contentType = args.contentType ?? mimeTypeFor2(filename);
|
|
1125
1138
|
const key = `${bundlePrefix2(args.slug)}${filename}`;
|
|
1126
|
-
const url = await storage.putObject(key, body, contentType);
|
|
1127
|
-
return { key, url, size: body.length };
|
|
1139
|
+
const { url } = await storage.putObject(key, body, contentType);
|
|
1140
|
+
return { key, url, size: body.length, contentType };
|
|
1128
1141
|
}
|
|
1129
1142
|
var deleteStaticFileSchema = {
|
|
1130
1143
|
type: "object",
|
|
@@ -1189,7 +1202,15 @@ async function commitStaticPost(graphql, storage, args) {
|
|
|
1189
1202
|
`commit_static_post: no files found under "${prefix}". Upload at least one file via upload_static_file or upload_static_bundle before committing.`
|
|
1190
1203
|
);
|
|
1191
1204
|
}
|
|
1192
|
-
const
|
|
1205
|
+
const filesMeta = {};
|
|
1206
|
+
const relPaths = [];
|
|
1207
|
+
for (const obj of objects) {
|
|
1208
|
+
const rel = obj.key.slice(prefix.length);
|
|
1209
|
+
if (rel === "") continue;
|
|
1210
|
+
relPaths.push(rel);
|
|
1211
|
+
filesMeta[rel] = { size: obj.size, mimeType: mimeTypeFor3(rel) };
|
|
1212
|
+
}
|
|
1213
|
+
relPaths.sort();
|
|
1193
1214
|
const entrypoint = args.entrypoint ?? pickDefaultEntrypoint2(relPaths.map((p) => ({ path: p })));
|
|
1194
1215
|
if (!relPaths.includes(entrypoint)) {
|
|
1195
1216
|
throw new Error(
|
|
@@ -1201,6 +1222,10 @@ async function commitStaticPost(graphql, storage, args) {
|
|
|
1201
1222
|
files: relPaths,
|
|
1202
1223
|
uploadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1203
1224
|
};
|
|
1225
|
+
const mergedMetadata = {
|
|
1226
|
+
...args.metadata ?? {},
|
|
1227
|
+
files: filesMeta
|
|
1228
|
+
};
|
|
1204
1229
|
const { post, created } = await upsertStaticPost(graphql, slug, body, {
|
|
1205
1230
|
title: args.title,
|
|
1206
1231
|
postId: args.postId,
|
|
@@ -1208,7 +1233,7 @@ async function commitStaticPost(graphql, storage, args) {
|
|
|
1208
1233
|
status: args.status,
|
|
1209
1234
|
publishedAt: args.publishedAt,
|
|
1210
1235
|
tags: args.tags,
|
|
1211
|
-
metadata:
|
|
1236
|
+
metadata: mergedMetadata
|
|
1212
1237
|
});
|
|
1213
1238
|
return { post, bundle: body, created };
|
|
1214
1239
|
}
|
|
@@ -1371,7 +1396,7 @@ function createMcpStorageClient(opts) {
|
|
|
1371
1396
|
const client = new S3Client({ region: opts.region });
|
|
1372
1397
|
return {
|
|
1373
1398
|
async putObject(key, body, contentType) {
|
|
1374
|
-
await client.send(
|
|
1399
|
+
const res = await client.send(
|
|
1375
1400
|
new PutObjectCommand({
|
|
1376
1401
|
Bucket: opts.bucket,
|
|
1377
1402
|
Key: key,
|
|
@@ -1379,7 +1404,11 @@ function createMcpStorageClient(opts) {
|
|
|
1379
1404
|
ContentType: contentType
|
|
1380
1405
|
})
|
|
1381
1406
|
);
|
|
1382
|
-
|
|
1407
|
+
const etag = res.ETag ? res.ETag.replace(/^"|"$/g, "") : void 0;
|
|
1408
|
+
return {
|
|
1409
|
+
url: formatPublicAssetUrl(opts.bucket, opts.region, key),
|
|
1410
|
+
etag
|
|
1411
|
+
};
|
|
1383
1412
|
},
|
|
1384
1413
|
async deleteObject(key) {
|
|
1385
1414
|
await client.send(
|
package/dist/index.d.ts
CHANGED
|
@@ -116,12 +116,13 @@ interface AmplessResolverPaths {
|
|
|
116
116
|
listPublishedPosts: string;
|
|
117
117
|
getPublishedPost: string;
|
|
118
118
|
listPostsByTag: string;
|
|
119
|
+
getMediaBySrc: string;
|
|
119
120
|
}
|
|
120
121
|
/**
|
|
121
122
|
* Default AppSync JS resolver paths. AppSync resolves these strings at
|
|
122
123
|
* CDK synth time relative to the file that calls `defineData` — i.e.
|
|
123
|
-
* the user's `amplify/data/resource.ts`. Templates ship the
|
|
124
|
-
*
|
|
124
|
+
* the user's `amplify/data/resource.ts`. Templates ship the resolver
|
|
125
|
+
* files at exactly these paths.
|
|
125
126
|
*
|
|
126
127
|
* Users with a non-default layout (e.g. moving the resolvers under a
|
|
127
128
|
* `resolvers/` subdir) pass overrides through `amplessSchemaModels(a, {
|
|
@@ -176,9 +177,11 @@ declare function amplessSchemaModels(a: any, opts?: AmplessSchemaModelsOpts): {
|
|
|
176
177
|
McpToken: any;
|
|
177
178
|
PublicPost: any;
|
|
178
179
|
PublicPostConnection: any;
|
|
180
|
+
PublicMedia: any;
|
|
179
181
|
listPublishedPosts: any;
|
|
180
182
|
getPublishedPost: any;
|
|
181
183
|
listPostsByTag: any;
|
|
184
|
+
getMediaBySrc: any;
|
|
182
185
|
};
|
|
183
186
|
/**
|
|
184
187
|
* Convenience helper: build a fully-instantiated `a.schema(...)`
|
package/dist/index.js
CHANGED
|
@@ -264,7 +264,8 @@ function amplessStorageConfig() {
|
|
|
264
264
|
var DEFAULT_RESOLVER_PATHS = {
|
|
265
265
|
listPublishedPosts: "./list-published-posts.js",
|
|
266
266
|
getPublishedPost: "./get-published-post.js",
|
|
267
|
-
listPostsByTag: "./list-posts-by-tag.js"
|
|
267
|
+
listPostsByTag: "./list-posts-by-tag.js",
|
|
268
|
+
getMediaBySrc: "./get-media-by-src.js"
|
|
268
269
|
};
|
|
269
270
|
function amplessSchemaModels(a, opts = {}) {
|
|
270
271
|
const resolverPaths = {
|
|
@@ -312,8 +313,14 @@ function amplessSchemaModels(a, opts = {}) {
|
|
|
312
313
|
src: a.string().required(),
|
|
313
314
|
mimeType: a.string().required(),
|
|
314
315
|
size: a.integer(),
|
|
315
|
-
delivery: a.string()
|
|
316
|
-
|
|
316
|
+
delivery: a.string(),
|
|
317
|
+
// Free-form per-asset metadata (JSON). Currently used to
|
|
318
|
+
// memoise the S3 ETag for stream-back routes; future use
|
|
319
|
+
// for image dimensions, EXIF strip status, etc. Kept loose
|
|
320
|
+
// because the field is read by routes that only need a few
|
|
321
|
+
// hints (no schema-level commitment).
|
|
322
|
+
metadata: a.json()
|
|
323
|
+
}).identifier(["mediaId"]).secondaryIndexes((index) => [index("src").name("bySrc")]).authorization((allow) => [
|
|
317
324
|
allow.groups(["ampless-admin", "ampless-editor"])
|
|
318
325
|
]),
|
|
319
326
|
Taxonomy: a.model({
|
|
@@ -415,6 +422,18 @@ function amplessSchemaModels(a, opts = {}) {
|
|
|
415
422
|
items: a.ref("PublicPost").array(),
|
|
416
423
|
nextToken: a.string()
|
|
417
424
|
}),
|
|
425
|
+
// Minimal Media projection for the public-facing `getMediaBySrc`
|
|
426
|
+
// query. Decoupling from the `Media` model lets the custom
|
|
427
|
+
// resolver bypass the model-level (admin/editor only) auth check
|
|
428
|
+
// on fields, and intentionally keeps `mediaId` / `delivery` /
|
|
429
|
+
// anything else off the wire — guests only need `size` /
|
|
430
|
+
// `mimeType` / `metadata` for the stream-back read path.
|
|
431
|
+
PublicMedia: a.customType({
|
|
432
|
+
src: a.string().required(),
|
|
433
|
+
size: a.integer(),
|
|
434
|
+
mimeType: a.string(),
|
|
435
|
+
metadata: a.json()
|
|
436
|
+
}),
|
|
418
437
|
// Public read endpoints — guard against draft leakage via custom resolvers.
|
|
419
438
|
// Custom handlers (`a.handler.custom`) only support apiKey / userPool /
|
|
420
439
|
// lambda / group / owner auth. `allow.guest()` (Cognito Identity Pool
|
|
@@ -466,6 +485,28 @@ function amplessSchemaModels(a, opts = {}) {
|
|
|
466
485
|
allow.publicApiKey(),
|
|
467
486
|
allow.groups(["ampless-admin", "ampless-editor"])
|
|
468
487
|
]),
|
|
488
|
+
// Public Media lookup by S3 key. Called by the `/api/media/...`
|
|
489
|
+
// route handler so guests can resolve `{ size, mimeType, metadata }`
|
|
490
|
+
// and the route can stream the bytes back with the right headers
|
|
491
|
+
// (and skip a HEAD round-trip on cold reads). The custom JS
|
|
492
|
+
// resolver targets the Media table's `bySrc` GSI directly so the
|
|
493
|
+
// lookup is one O(1) Query.
|
|
494
|
+
//
|
|
495
|
+
// Auth: `allow.publicApiKey()` — same model as the post queries
|
|
496
|
+
// above. `a.handler.custom` doesn't accept `allow.guest()` in
|
|
497
|
+
// Amplify Gen 2, so the API key (auto-renewed every 365 days) is
|
|
498
|
+
// the standard public-read channel. The resolver returns only the
|
|
499
|
+
// narrow `PublicMedia` projection — no `mediaId` / `delivery` /
|
|
500
|
+
// anything else leaks to guests.
|
|
501
|
+
getMediaBySrc: a.query().arguments({ src: a.string().required() }).returns(a.ref("PublicMedia")).handler(
|
|
502
|
+
a.handler.custom({
|
|
503
|
+
dataSource: a.ref("Media"),
|
|
504
|
+
entry: resolverPaths.getMediaBySrc
|
|
505
|
+
})
|
|
506
|
+
).authorization((allow) => [
|
|
507
|
+
allow.publicApiKey(),
|
|
508
|
+
allow.groups(["ampless-admin", "ampless-editor"])
|
|
509
|
+
]),
|
|
469
510
|
// User management ops, only wired when the caller supplies a
|
|
470
511
|
// Lambda function ref. Conditionally spread because
|
|
471
512
|
// `a.handler.function(undefined)` is not a valid call — projects
|
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.31",
|
|
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.16",
|
|
69
|
+
"@ampless/mcp-server": "1.0.0-alpha.20"
|
|
70
70
|
},
|
|
71
71
|
"peerDependencies": {
|
|
72
72
|
"@aws-amplify/backend": "^1",
|