@beignet/core 0.0.7 → 0.0.9
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/CHANGELOG.md +27 -0
- package/README.md +47 -9
- package/dist/idempotency/index.d.ts.map +1 -1
- package/dist/idempotency/index.js +6 -1
- package/dist/idempotency/index.js.map +1 -1
- package/dist/mail/index.d.ts +29 -0
- package/dist/mail/index.d.ts.map +1 -1
- package/dist/mail/index.js +42 -0
- package/dist/mail/index.js.map +1 -1
- package/dist/notifications/index.d.ts +31 -0
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +29 -1
- package/dist/notifications/index.js.map +1 -1
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/metadata.d.ts +8 -0
- package/dist/providers/metadata.d.ts.map +1 -1
- package/dist/providers/metadata.js +100 -3
- package/dist/providers/metadata.js.map +1 -1
- package/dist/uploads/index.d.ts.map +1 -1
- package/dist/uploads/index.js +102 -10
- package/dist/uploads/index.js.map +1 -1
- package/package.json +1 -1
- package/src/idempotency/index.ts +6 -1
- package/src/mail/index.ts +71 -0
- package/src/notifications/index.ts +61 -1
- package/src/providers/index.ts +1 -0
- package/src/providers/metadata.ts +126 -2
- package/src/uploads/index.ts +139 -14
|
@@ -19,6 +19,14 @@ export type ProviderPackageRegistrationMetadata = {
|
|
|
19
19
|
tokens?: readonly string[];
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
export type ProviderPackageVariantMetadata = {
|
|
23
|
+
name: string;
|
|
24
|
+
displayName?: string;
|
|
25
|
+
env?: readonly string[];
|
|
26
|
+
requiredEnv?: readonly string[];
|
|
27
|
+
registration?: ProviderPackageRegistrationMetadata;
|
|
28
|
+
};
|
|
29
|
+
|
|
22
30
|
export type ProviderPackageMetadata = {
|
|
23
31
|
displayName?: string;
|
|
24
32
|
ports?: readonly string[];
|
|
@@ -26,6 +34,7 @@ export type ProviderPackageMetadata = {
|
|
|
26
34
|
env?: readonly string[];
|
|
27
35
|
requiredEnv?: readonly string[];
|
|
28
36
|
registration?: ProviderPackageRegistrationMetadata;
|
|
37
|
+
variants?: readonly ProviderPackageVariantMetadata[];
|
|
29
38
|
watchers?: readonly string[];
|
|
30
39
|
};
|
|
31
40
|
|
|
@@ -101,9 +110,33 @@ export function parseProviderPackageMetadata(
|
|
|
101
110
|
const appPorts = parseAppPorts(input.appPorts, issues);
|
|
102
111
|
if (appPorts !== undefined) metadata.appPorts = appPorts;
|
|
103
112
|
|
|
104
|
-
const registration = parseRegistration(
|
|
113
|
+
const registration = parseRegistration(
|
|
114
|
+
input.registration,
|
|
115
|
+
issues,
|
|
116
|
+
"beignet.provider.registration",
|
|
117
|
+
);
|
|
105
118
|
if (registration !== undefined) metadata.registration = registration;
|
|
106
119
|
|
|
120
|
+
const variants = parseVariants(input.variants, issues);
|
|
121
|
+
if (variants !== undefined) metadata.variants = variants;
|
|
122
|
+
|
|
123
|
+
if (input.variants !== undefined) {
|
|
124
|
+
if (input.requiredEnv !== undefined) {
|
|
125
|
+
issues.push({
|
|
126
|
+
path: "beignet.provider.requiredEnv",
|
|
127
|
+
message:
|
|
128
|
+
"must not be set when beignet.provider.variants is present; declare requiredEnv on each variant instead",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (input.registration !== undefined) {
|
|
132
|
+
issues.push({
|
|
133
|
+
path: "beignet.provider.registration",
|
|
134
|
+
message:
|
|
135
|
+
"must not be set when beignet.provider.variants is present; declare registration on each variant instead",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
107
140
|
if (issues.length > 0) {
|
|
108
141
|
return { success: false, issues };
|
|
109
142
|
}
|
|
@@ -169,12 +202,103 @@ function parseAppPorts(
|
|
|
169
202
|
return parsed;
|
|
170
203
|
}
|
|
171
204
|
|
|
205
|
+
function parseVariants(
|
|
206
|
+
value: unknown,
|
|
207
|
+
issues: ProviderPackageMetadataIssue[],
|
|
208
|
+
): ProviderPackageVariantMetadata[] | undefined {
|
|
209
|
+
if (value === undefined) return undefined;
|
|
210
|
+
const path = "beignet.provider.variants";
|
|
211
|
+
if (!Array.isArray(value)) {
|
|
212
|
+
issues.push({ path, message: "must be an array of variant objects" });
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
if (value.length === 0) {
|
|
216
|
+
issues.push({
|
|
217
|
+
path,
|
|
218
|
+
message: "must include at least one variant when present",
|
|
219
|
+
});
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const parsed: ProviderPackageVariantMetadata[] = [];
|
|
224
|
+
const seenNames = new Set<string>();
|
|
225
|
+
for (const [index, entry] of value.entries()) {
|
|
226
|
+
const entryPath = `${path}[${index}]`;
|
|
227
|
+
if (!isRecord(entry)) {
|
|
228
|
+
issues.push({ path: entryPath, message: "must be an object" });
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let name: string | undefined;
|
|
233
|
+
if (typeof entry.name !== "string" || entry.name.length === 0) {
|
|
234
|
+
issues.push({
|
|
235
|
+
path: `${entryPath}.name`,
|
|
236
|
+
message: "must be a non-empty string",
|
|
237
|
+
});
|
|
238
|
+
} else if (seenNames.has(entry.name)) {
|
|
239
|
+
issues.push({
|
|
240
|
+
path: `${entryPath}.name`,
|
|
241
|
+
message: `duplicates variant name "${entry.name}"`,
|
|
242
|
+
});
|
|
243
|
+
} else {
|
|
244
|
+
seenNames.add(entry.name);
|
|
245
|
+
name = entry.name;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let displayName: string | undefined;
|
|
249
|
+
if (entry.displayName !== undefined) {
|
|
250
|
+
if (typeof entry.displayName !== "string") {
|
|
251
|
+
issues.push({
|
|
252
|
+
path: `${entryPath}.displayName`,
|
|
253
|
+
message: "must be a string",
|
|
254
|
+
});
|
|
255
|
+
} else {
|
|
256
|
+
displayName = entry.displayName;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const env = parseStringArray(entry.env, `${entryPath}.env`, issues);
|
|
261
|
+
const requiredEnv = parseStringArray(
|
|
262
|
+
entry.requiredEnv,
|
|
263
|
+
`${entryPath}.requiredEnv`,
|
|
264
|
+
issues,
|
|
265
|
+
);
|
|
266
|
+
if (requiredEnv !== undefined) {
|
|
267
|
+
const envSet = new Set(env ?? []);
|
|
268
|
+
for (const envVar of requiredEnv) {
|
|
269
|
+
if (!envSet.has(envVar)) {
|
|
270
|
+
issues.push({
|
|
271
|
+
path: `${entryPath}.requiredEnv`,
|
|
272
|
+
message: `${envVar} must also be listed in ${entryPath}.env`,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const registration = parseRegistration(
|
|
279
|
+
entry.registration,
|
|
280
|
+
issues,
|
|
281
|
+
`${entryPath}.registration`,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
if (name === undefined) continue;
|
|
285
|
+
const variant: ProviderPackageVariantMetadata = { name };
|
|
286
|
+
if (displayName !== undefined) variant.displayName = displayName;
|
|
287
|
+
if (env !== undefined) variant.env = env;
|
|
288
|
+
if (requiredEnv !== undefined) variant.requiredEnv = requiredEnv;
|
|
289
|
+
if (registration !== undefined) variant.registration = registration;
|
|
290
|
+
parsed.push(variant);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return parsed;
|
|
294
|
+
}
|
|
295
|
+
|
|
172
296
|
function parseRegistration(
|
|
173
297
|
value: unknown,
|
|
174
298
|
issues: ProviderPackageMetadataIssue[],
|
|
299
|
+
path: string,
|
|
175
300
|
): ProviderPackageRegistrationMetadata | undefined {
|
|
176
301
|
if (value === undefined) return undefined;
|
|
177
|
-
const path = "beignet.provider.registration";
|
|
178
302
|
if (!isRecord(value)) {
|
|
179
303
|
issues.push({ path, message: "must be an object" });
|
|
180
304
|
return undefined;
|
package/src/uploads/index.ts
CHANGED
|
@@ -541,9 +541,15 @@ export function createMemoryUploadSigner(
|
|
|
541
541
|
export function createUploadRouter<Ctx>(
|
|
542
542
|
options: CreateUploadRouterOptions<Ctx>,
|
|
543
543
|
): UploadRouter {
|
|
544
|
-
const uploads = new Map(
|
|
545
|
-
|
|
546
|
-
|
|
544
|
+
const uploads = new Map<string, UploadDef<string, StandardSchema, Ctx>>();
|
|
545
|
+
for (const upload of options.uploads) {
|
|
546
|
+
if (uploads.has(upload.name)) {
|
|
547
|
+
throw new Error(
|
|
548
|
+
`createUploadRouter received duplicate upload name "${upload.name}". Each defineUpload(...) name must be unique.`,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
uploads.set(upload.name, upload);
|
|
552
|
+
}
|
|
547
553
|
const id = options.id ?? randomUploadId;
|
|
548
554
|
const instrumentation = createProviderInstrumentation(
|
|
549
555
|
options.instrumentation,
|
|
@@ -562,10 +568,13 @@ export function createUploadRouter<Ctx>(
|
|
|
562
568
|
function findUpload(name: string) {
|
|
563
569
|
const upload = uploads.get(name);
|
|
564
570
|
if (!upload) {
|
|
571
|
+
const registered = [...uploads.keys()]
|
|
572
|
+
.map((registeredName) => `"${registeredName}"`)
|
|
573
|
+
.join(", ");
|
|
565
574
|
throw new UploadError({
|
|
566
575
|
code: "UPLOAD_NOT_FOUND",
|
|
567
576
|
status: 404,
|
|
568
|
-
message: `Upload "${name}" is not registered.`,
|
|
577
|
+
message: `Upload "${name}" is not registered. Registered uploads: ${registered || "none"}. Upload routes resolve the defineUpload(...) name, not the defineUploads({...}) registry key.`,
|
|
569
578
|
});
|
|
570
579
|
}
|
|
571
580
|
return upload;
|
|
@@ -581,12 +590,13 @@ export function createUploadRouter<Ctx>(
|
|
|
581
590
|
|
|
582
591
|
try {
|
|
583
592
|
const upload = findUpload(uploadName);
|
|
593
|
+
const parsed = parsePrepareInput(uploadName, input);
|
|
584
594
|
const ctx = await resolveCtx();
|
|
585
|
-
const metadata = await parseMetadata(upload,
|
|
586
|
-
assertFiles(upload,
|
|
595
|
+
const metadata = await parseMetadata(upload, parsed.metadata);
|
|
596
|
+
assertFiles(upload, parsed.files);
|
|
587
597
|
|
|
588
598
|
const files: PreparedUploadResultFile[] = [];
|
|
589
|
-
for (const file of
|
|
599
|
+
for (const file of parsed.files) {
|
|
590
600
|
const uploadId = id();
|
|
591
601
|
await assertAuthorized(upload, { ctx, metadata, file, uploadId });
|
|
592
602
|
const key = await upload.key({ ctx, metadata, file, uploadId });
|
|
@@ -652,12 +662,13 @@ export function createUploadRouter<Ctx>(
|
|
|
652
662
|
|
|
653
663
|
try {
|
|
654
664
|
const upload = findUpload(uploadName);
|
|
665
|
+
const parsed = parseCompleteInput(uploadName, input);
|
|
655
666
|
const ctx = await resolveCtx();
|
|
656
|
-
const metadata = await parseMetadata(upload,
|
|
657
|
-
assertFiles(upload,
|
|
667
|
+
const metadata = await parseMetadata(upload, parsed.metadata);
|
|
668
|
+
assertFiles(upload, parsed.files);
|
|
658
669
|
|
|
659
670
|
const files: CompletedUploadFile[] = [];
|
|
660
|
-
for (const file of
|
|
671
|
+
for (const file of parsed.files) {
|
|
661
672
|
await assertAuthorized(upload, {
|
|
662
673
|
ctx,
|
|
663
674
|
metadata,
|
|
@@ -827,13 +838,29 @@ export function createUploadRouter<Ctx>(
|
|
|
827
838
|
async handleRequest(request, requestOptions) {
|
|
828
839
|
try {
|
|
829
840
|
if (requestOptions.action === "prepare") {
|
|
830
|
-
const input =
|
|
831
|
-
|
|
841
|
+
const input = await readJsonBody(request, {
|
|
842
|
+
uploadName: requestOptions.uploadName,
|
|
843
|
+
action: "prepare",
|
|
844
|
+
});
|
|
845
|
+
return jsonResponse(
|
|
846
|
+
await prepare(
|
|
847
|
+
requestOptions.uploadName,
|
|
848
|
+
input as PrepareUploadInput,
|
|
849
|
+
),
|
|
850
|
+
);
|
|
832
851
|
}
|
|
833
852
|
|
|
834
853
|
if (requestOptions.action === "complete") {
|
|
835
|
-
const input =
|
|
836
|
-
|
|
854
|
+
const input = await readJsonBody(request, {
|
|
855
|
+
uploadName: requestOptions.uploadName,
|
|
856
|
+
action: "complete",
|
|
857
|
+
});
|
|
858
|
+
return jsonResponse(
|
|
859
|
+
await complete(
|
|
860
|
+
requestOptions.uploadName,
|
|
861
|
+
input as CompleteUploadInput,
|
|
862
|
+
),
|
|
863
|
+
);
|
|
837
864
|
}
|
|
838
865
|
|
|
839
866
|
const formData = await request.formData();
|
|
@@ -849,6 +876,104 @@ export function createUploadRouter<Ctx>(
|
|
|
849
876
|
};
|
|
850
877
|
}
|
|
851
878
|
|
|
879
|
+
async function readJsonBody(
|
|
880
|
+
request: Request,
|
|
881
|
+
context: { uploadName: string; action: "prepare" | "complete" },
|
|
882
|
+
): Promise<unknown> {
|
|
883
|
+
try {
|
|
884
|
+
return await request.json();
|
|
885
|
+
} catch {
|
|
886
|
+
throw new UploadError({
|
|
887
|
+
code: "INVALID_UPLOAD_BODY",
|
|
888
|
+
status: 400,
|
|
889
|
+
message: `Upload "${context.uploadName}" ${context.action} body must be valid JSON.`,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
interface UploadBodyIssue {
|
|
895
|
+
message: string;
|
|
896
|
+
path: (string | number)[];
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function collectBodyIssues(
|
|
900
|
+
body: unknown,
|
|
901
|
+
options: { requireCompletedFileFields: boolean },
|
|
902
|
+
): UploadBodyIssue[] {
|
|
903
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
904
|
+
return [{ message: "Body must be a JSON object.", path: [] }];
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const files = (body as { files?: unknown }).files;
|
|
908
|
+
if (!Array.isArray(files)) {
|
|
909
|
+
return [{ message: 'Body must include a "files" array.', path: ["files"] }];
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const issues: UploadBodyIssue[] = [];
|
|
913
|
+
for (const [index, file] of files.entries()) {
|
|
914
|
+
if (typeof file !== "object" || file === null || Array.isArray(file)) {
|
|
915
|
+
issues.push({
|
|
916
|
+
message: "Each file must be an object.",
|
|
917
|
+
path: ["files", index],
|
|
918
|
+
});
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (!options.requireCompletedFileFields) continue;
|
|
923
|
+
|
|
924
|
+
const completed = file as { uploadId?: unknown; key?: unknown };
|
|
925
|
+
if (typeof completed.uploadId !== "string") {
|
|
926
|
+
issues.push({
|
|
927
|
+
message: 'Each completed file must include a string "uploadId".',
|
|
928
|
+
path: ["files", index, "uploadId"],
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
if (typeof completed.key !== "string") {
|
|
932
|
+
issues.push({
|
|
933
|
+
message: 'Each completed file must include a string "key".',
|
|
934
|
+
path: ["files", index, "key"],
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return issues;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function parsePrepareInput(
|
|
942
|
+
uploadName: string,
|
|
943
|
+
body: unknown,
|
|
944
|
+
): PrepareUploadInput {
|
|
945
|
+
const issues = collectBodyIssues(body, {
|
|
946
|
+
requireCompletedFileFields: false,
|
|
947
|
+
});
|
|
948
|
+
if (issues.length > 0) {
|
|
949
|
+
throw new UploadError({
|
|
950
|
+
code: "INVALID_UPLOAD_BODY",
|
|
951
|
+
status: 400,
|
|
952
|
+
message: `Upload "${uploadName}" prepare body is invalid.`,
|
|
953
|
+
details: { issues },
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
return body as PrepareUploadInput;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function parseCompleteInput(
|
|
960
|
+
uploadName: string,
|
|
961
|
+
body: unknown,
|
|
962
|
+
): CompleteUploadInput {
|
|
963
|
+
const issues = collectBodyIssues(body, {
|
|
964
|
+
requireCompletedFileFields: true,
|
|
965
|
+
});
|
|
966
|
+
if (issues.length > 0) {
|
|
967
|
+
throw new UploadError({
|
|
968
|
+
code: "INVALID_UPLOAD_BODY",
|
|
969
|
+
status: 400,
|
|
970
|
+
message: `Upload "${uploadName}" complete body is invalid.`,
|
|
971
|
+
details: { issues },
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
return body as CompleteUploadInput;
|
|
975
|
+
}
|
|
976
|
+
|
|
852
977
|
async function parseMetadata<U extends UploadDef>(
|
|
853
978
|
upload: U,
|
|
854
979
|
input: unknown,
|