@flaredev/core 0.10.0 → 0.12.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.
- package/dist/chunk-7NQIVWGG.js +68 -0
- package/dist/{chunk-TB53VHXY.js → chunk-BTVEH5AU.js} +383 -12
- package/dist/chunk-I2HSIPCF.js +65 -0
- package/dist/index.d.ts +121 -5
- package/dist/index.js +75 -217
- package/dist/{policy-DFimkxdx.d.ts → policy-b4FXN3ZJ.d.ts} +196 -2
- package/dist/rate-limit/server.d.ts +44 -0
- package/dist/rate-limit/server.js +45 -0
- package/dist/rate-limit-ClbdZioJ.d.ts +97 -0
- package/dist/security/worker.js +13 -5
- package/dist/server.d.ts +69 -62
- package/dist/server.js +116 -37
- package/package.json +5 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/problem.ts
|
|
2
|
+
var PROBLEM_CODES = {
|
|
3
|
+
/** The body broke a field's rules. `issues` says which fields. */
|
|
4
|
+
validationFailed: "validation_failed",
|
|
5
|
+
/** A unique column already holds this value. `issues` names the column. */
|
|
6
|
+
uniqueViolation: "unique_violation",
|
|
7
|
+
/** Deleting this would orphan rows that point at it. */
|
|
8
|
+
stillReferenced: "still_referenced",
|
|
9
|
+
/** A relation field names a record that does not exist. */
|
|
10
|
+
relatedRecordMissing: "related_record_missing",
|
|
11
|
+
/** A value outside what its column accepts — an enum option that is not one. */
|
|
12
|
+
valueNotAllowed: "value_not_allowed",
|
|
13
|
+
/** No such record, or none this request may see. */
|
|
14
|
+
notFound: "not_found",
|
|
15
|
+
/** A query parameter is wrong. `issues` names the parameters. */
|
|
16
|
+
invalidQuery: "invalid_query",
|
|
17
|
+
/** The body was not JSON. */
|
|
18
|
+
invalidJson: "invalid_json",
|
|
19
|
+
/** The request did not say `Content-Type: application/json`. */
|
|
20
|
+
unsupportedMediaType: "unsupported_media_type",
|
|
21
|
+
/** A write from another origin: the CSRF guard. */
|
|
22
|
+
crossOriginBlocked: "cross_origin_blocked",
|
|
23
|
+
/** No session and no API key. */
|
|
24
|
+
unauthenticated: "unauthenticated",
|
|
25
|
+
/** Signed in, but this role may not do this. */
|
|
26
|
+
forbidden: "forbidden",
|
|
27
|
+
/** Refused by the security layer before the app saw it. */
|
|
28
|
+
accessDenied: "access_denied",
|
|
29
|
+
/** Too many requests. */
|
|
30
|
+
rateLimited: "rate_limited",
|
|
31
|
+
/** A signed storage URL that is missing, malformed, expired, or not for this operation. */
|
|
32
|
+
invalidToken: "invalid_token",
|
|
33
|
+
/** An upload past the limit the signed URL allows. */
|
|
34
|
+
payloadTooLarge: "payload_too_large",
|
|
35
|
+
/** An upload with no Content-Length, which cannot be checked against that limit. */
|
|
36
|
+
lengthRequired: "length_required",
|
|
37
|
+
/** A request that is wrong in a way none of the others describes. */
|
|
38
|
+
badRequest: "bad_request",
|
|
39
|
+
/** Something went wrong that the server does not explain. */
|
|
40
|
+
internal: "internal"
|
|
41
|
+
};
|
|
42
|
+
var ISSUE_CODES = {
|
|
43
|
+
/** Broke the field's own rules: a format, a length, a required value. */
|
|
44
|
+
invalid: "invalid",
|
|
45
|
+
/** A unique column already holds this. */
|
|
46
|
+
unique: "unique",
|
|
47
|
+
/** Set once and not changeable — an ownership column, for instance. */
|
|
48
|
+
immutable: "immutable",
|
|
49
|
+
/** A query parameter rather than a body field. */
|
|
50
|
+
invalidParameter: "invalid_parameter"
|
|
51
|
+
};
|
|
52
|
+
function wantsJson(request) {
|
|
53
|
+
const accept = request.headers.get("accept") ?? "";
|
|
54
|
+
if (/\bapplication\/json\b/i.test(accept)) return true;
|
|
55
|
+
if (/\btext\/html\b/i.test(accept)) return false;
|
|
56
|
+
return new URL(request.url).pathname.startsWith("/api/");
|
|
57
|
+
}
|
|
58
|
+
function problemResponse(status, error, code, issues, init) {
|
|
59
|
+
const body = issues?.length ? { error, code, issues } : { error, code };
|
|
60
|
+
return Response.json(body, { ...init, status });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
PROBLEM_CODES,
|
|
65
|
+
ISSUE_CODES,
|
|
66
|
+
wantsJson,
|
|
67
|
+
problemResponse
|
|
68
|
+
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseRate
|
|
3
|
+
} from "./chunk-I2HSIPCF.js";
|
|
4
|
+
|
|
1
5
|
// src/resource/fields.ts
|
|
2
6
|
var STRING_FORMATS = [
|
|
3
7
|
"email",
|
|
@@ -160,13 +164,50 @@ var humanize = (value) => {
|
|
|
160
164
|
const text = words(value).join(" ");
|
|
161
165
|
return text ? text[0].toUpperCase() + text.slice(1) : text;
|
|
162
166
|
};
|
|
163
|
-
var UNCOUNTABLE = /* @__PURE__ */ new Set(["equipment", "information", "rice", "money", "species", "series", "fish", "sheep", "data", "news"]);
|
|
164
|
-
var IRREGULAR = {
|
|
167
|
+
var UNCOUNTABLE = /* @__PURE__ */ new Set(["equipment", "information", "rice", "money", "species", "series", "fish", "sheep", "data", "news", "staff"]);
|
|
168
|
+
var IRREGULAR = {
|
|
169
|
+
person: "people",
|
|
170
|
+
man: "men",
|
|
171
|
+
woman: "women",
|
|
172
|
+
child: "children",
|
|
173
|
+
mouse: "mice",
|
|
174
|
+
// -f / -fe → -ves
|
|
175
|
+
shelf: "shelves",
|
|
176
|
+
half: "halves",
|
|
177
|
+
knife: "knives",
|
|
178
|
+
life: "lives",
|
|
179
|
+
leaf: "leaves",
|
|
180
|
+
loaf: "loaves",
|
|
181
|
+
wolf: "wolves",
|
|
182
|
+
wife: "wives",
|
|
183
|
+
calf: "calves",
|
|
184
|
+
self: "selves",
|
|
185
|
+
thief: "thieves",
|
|
186
|
+
elf: "elves",
|
|
187
|
+
// Latin endings an app meets in a catalogue or a lab
|
|
188
|
+
index: "indexes",
|
|
189
|
+
matrix: "matrices",
|
|
190
|
+
vertex: "vertices",
|
|
191
|
+
appendix: "appendices",
|
|
192
|
+
analysis: "analyses",
|
|
193
|
+
diagnosis: "diagnoses",
|
|
194
|
+
thesis: "theses",
|
|
195
|
+
basis: "bases",
|
|
196
|
+
crisis: "crises",
|
|
197
|
+
criterion: "criteria",
|
|
198
|
+
curriculum: "curricula",
|
|
199
|
+
syllabus: "syllabuses",
|
|
200
|
+
alumnus: "alumni",
|
|
201
|
+
campus: "campuses",
|
|
202
|
+
status: "statuses",
|
|
203
|
+
quiz: "quizzes"
|
|
204
|
+
};
|
|
165
205
|
function pluralize(value) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
206
|
+
if (!/[A-Za-z]$/.test(value)) return value;
|
|
207
|
+
const parts = words(value);
|
|
208
|
+
const lower = parts[parts.length - 1];
|
|
209
|
+
if (!lower) return value;
|
|
210
|
+
const word = value.slice(value.length - lower.length);
|
|
170
211
|
const head = value.slice(0, value.length - word.length);
|
|
171
212
|
const keepCase = (plural) => word[0] === word[0].toUpperCase() ? plural[0].toUpperCase() + plural.slice(1) : plural;
|
|
172
213
|
if (UNCOUNTABLE.has(lower)) return value;
|
|
@@ -192,6 +233,17 @@ var ResourceDefinitionError = class extends Error {
|
|
|
192
233
|
}
|
|
193
234
|
};
|
|
194
235
|
var IDENTIFIER = /^[a-z][A-Za-z0-9]*$/;
|
|
236
|
+
function validateRateLimit(name, config) {
|
|
237
|
+
for (const [action, spec] of Object.entries(config)) {
|
|
238
|
+
if (typeof spec !== "string") continue;
|
|
239
|
+
try {
|
|
240
|
+
parseRate(spec);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
throw new ResourceDefinitionError(name, `rateLimit.${action}: ${error instanceof Error ? error.message : String(error)}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return config;
|
|
246
|
+
}
|
|
195
247
|
function defaultFieldLabel(key, field2) {
|
|
196
248
|
return humanize(field2.kind === "belongsTo" ? key.replace(/Id$/, "") : key);
|
|
197
249
|
}
|
|
@@ -237,7 +289,7 @@ function defineResource(config) {
|
|
|
237
289
|
fail(`titleField "${config.titleField}" isn't a stored field.`);
|
|
238
290
|
}
|
|
239
291
|
const defaultSort = config.defaultSort ?? { field: "createdAt", direction: "desc" };
|
|
240
|
-
if (!["createdAt", "updatedAt"].includes(defaultSort.field) && !stored.some(([key]) => key === defaultSort.field)) {
|
|
292
|
+
if (!["id", "createdAt", "updatedAt"].includes(defaultSort.field) && !stored.some(([key]) => key === defaultSort.field)) {
|
|
241
293
|
fail(`defaultSort field "${defaultSort.field}" isn't a stored field.`);
|
|
242
294
|
}
|
|
243
295
|
const perPage = config.perPage ?? 25;
|
|
@@ -257,6 +309,9 @@ function defineResource(config) {
|
|
|
257
309
|
defaultSort,
|
|
258
310
|
perPage,
|
|
259
311
|
softDelete: config.softDelete === true,
|
|
312
|
+
// Parsed here rather than in the route, so a rate that cannot be read is an error at
|
|
313
|
+
// `flare gen` with the descriptor in front of you, not a surprise in production.
|
|
314
|
+
...config.rateLimit ? { rateLimit: validateRateLimit(config.name, config.rateLimit) } : {},
|
|
260
315
|
fields,
|
|
261
316
|
$types: void 0
|
|
262
317
|
};
|
|
@@ -266,6 +321,65 @@ function storedFields(resource) {
|
|
|
266
321
|
}
|
|
267
322
|
var columnName = (key) => snakeCase(key);
|
|
268
323
|
|
|
324
|
+
// src/resource/conditions.ts
|
|
325
|
+
function conditionHolds(condition, values) {
|
|
326
|
+
if (!condition) return true;
|
|
327
|
+
const actual = values[condition.field];
|
|
328
|
+
const text = actual === null || actual === void 0 ? "" : String(actual);
|
|
329
|
+
switch (condition.op) {
|
|
330
|
+
case "eq":
|
|
331
|
+
return text === String(condition.value);
|
|
332
|
+
case "ne":
|
|
333
|
+
return text !== String(condition.value);
|
|
334
|
+
case "in":
|
|
335
|
+
return (Array.isArray(condition.value) ? condition.value : [condition.value]).some((option) => String(option) === text);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function describeCondition(condition) {
|
|
339
|
+
const values = Array.isArray(condition.value) ? condition.value : [condition.value];
|
|
340
|
+
if (condition.op === "in") return `${condition.field} is one of ${values.join(", ")}`;
|
|
341
|
+
return `${condition.field} ${condition.op === "eq" ? "is" : "is not"} ${values[0]}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/resource/enum-member.ts
|
|
345
|
+
var IDENTIFIER_CHAR = /[A-Za-z0-9_]/;
|
|
346
|
+
function prismaEnumMember(option) {
|
|
347
|
+
let member = "";
|
|
348
|
+
for (const char of option) {
|
|
349
|
+
if (IDENTIFIER_CHAR.test(char)) member += char;
|
|
350
|
+
else if (char === "-" || char === " ") member += "_";
|
|
351
|
+
else member += `_${char.codePointAt(0).toString(16)}`;
|
|
352
|
+
}
|
|
353
|
+
return /^\d/.test(member) ? `v_${member}` : member;
|
|
354
|
+
}
|
|
355
|
+
function enumMemberCollision(options) {
|
|
356
|
+
const seen = /* @__PURE__ */ new Map();
|
|
357
|
+
for (const option of options) {
|
|
358
|
+
const member = prismaEnumMember(option);
|
|
359
|
+
const first = seen.get(member);
|
|
360
|
+
if (first !== void 0) return { a: first, b: option, member };
|
|
361
|
+
seen.set(member, option);
|
|
362
|
+
}
|
|
363
|
+
return void 0;
|
|
364
|
+
}
|
|
365
|
+
function enumTranslations(fields) {
|
|
366
|
+
const out = {};
|
|
367
|
+
for (const [key, def] of fields) {
|
|
368
|
+
if (def.kind !== "enum") continue;
|
|
369
|
+
const options = def.options ?? [];
|
|
370
|
+
if (options.every((option) => prismaEnumMember(option) === option)) continue;
|
|
371
|
+
const toMember = {};
|
|
372
|
+
const toValue = {};
|
|
373
|
+
for (const option of options) {
|
|
374
|
+
const member = prismaEnumMember(option);
|
|
375
|
+
toMember[option] = member;
|
|
376
|
+
toValue[member] = option;
|
|
377
|
+
}
|
|
378
|
+
out[key] = { toMember, toValue };
|
|
379
|
+
}
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
|
|
269
383
|
// src/resource/formats.ts
|
|
270
384
|
import {
|
|
271
385
|
getCountries,
|
|
@@ -337,7 +451,27 @@ var isSlug = (value) => /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
|
|
|
337
451
|
function slugify(value) {
|
|
338
452
|
return value.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
339
453
|
}
|
|
340
|
-
|
|
454
|
+
function isIpv4(value) {
|
|
455
|
+
if (!/^(?:\d{1,3}\.){3}\d{1,3}$/.test(value)) return false;
|
|
456
|
+
return value.split(".").every((part) => {
|
|
457
|
+
const num = Number(part);
|
|
458
|
+
return num >= 0 && num <= 255;
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function isIpv6(value) {
|
|
462
|
+
if (!value || value.includes(":::")) return false;
|
|
463
|
+
const doubleColonParts = value.split("::");
|
|
464
|
+
if (doubleColonParts.length > 2) return false;
|
|
465
|
+
if (doubleColonParts.length === 2) {
|
|
466
|
+
const left = doubleColonParts[0] ? doubleColonParts[0].split(":") : [];
|
|
467
|
+
const right = doubleColonParts[1] ? doubleColonParts[1].split(":") : [];
|
|
468
|
+
if (left.length + right.length >= 8) return false;
|
|
469
|
+
return [...left, ...right].every((p) => /^[0-9a-f]{1,4}$/i.test(p));
|
|
470
|
+
}
|
|
471
|
+
const parts = value.split(":");
|
|
472
|
+
return parts.length === 8 && parts.every((p) => /^[0-9a-f]{1,4}$/i.test(p));
|
|
473
|
+
}
|
|
474
|
+
var isIpAddress = (value) => isIpv4(value) || isIpv6(value);
|
|
341
475
|
var isUuid = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
342
476
|
function isTimezone(value) {
|
|
343
477
|
try {
|
|
@@ -480,18 +614,238 @@ function createValidators(resource) {
|
|
|
480
614
|
"This file wasn't uploaded for this field"
|
|
481
615
|
);
|
|
482
616
|
}
|
|
483
|
-
const
|
|
617
|
+
const conditional2 = def.showWhen !== void 0;
|
|
618
|
+
const value = def.required && !conditional2 ? base : base.nullable();
|
|
484
619
|
update[key] = value.optional();
|
|
485
620
|
const hasDefault = "default" in def && def.default !== void 0;
|
|
486
|
-
create[key] = def.required && !hasDefault ? value : value.optional();
|
|
621
|
+
create[key] = def.required && !conditional2 && !hasDefault ? value : value.optional();
|
|
487
622
|
}
|
|
623
|
+
const conditional = [...storedFields(resource)].flatMap(
|
|
624
|
+
([key, def]) => def.showWhen ? [{ key, condition: def.showWhen, required: def.required !== false }] : []
|
|
625
|
+
);
|
|
626
|
+
const checkConditions = (partial) => (values, ctx) => {
|
|
627
|
+
for (const { key, condition, required } of conditional) {
|
|
628
|
+
if (!required) continue;
|
|
629
|
+
if (partial && !(condition.field in values) && !(key in values)) continue;
|
|
630
|
+
if (!conditionHolds(condition, values)) continue;
|
|
631
|
+
const supplied = values[key];
|
|
632
|
+
if (supplied === void 0 || supplied === null || supplied === "") {
|
|
633
|
+
ctx.addIssue({ code: "custom", path: [key], message: `Required when ${describeCondition(condition)}` });
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
};
|
|
488
637
|
const unknownKeys = { error: (issue) => issue.code === "unrecognized_keys" ? `Unknown field(s): ${issue.keys?.join(", ")}` : void 0 };
|
|
489
638
|
return {
|
|
490
|
-
create: z.strictObject(create, unknownKeys),
|
|
491
|
-
update: z.strictObject(update, unknownKeys)
|
|
639
|
+
create: z.strictObject(create, unknownKeys).superRefine(checkConditions(false)),
|
|
640
|
+
update: z.strictObject(update, unknownKeys).superRefine(checkConditions(true))
|
|
492
641
|
};
|
|
493
642
|
}
|
|
494
643
|
|
|
644
|
+
// src/resource/relations.ts
|
|
645
|
+
var RelationError = class extends Error {
|
|
646
|
+
constructor(message) {
|
|
647
|
+
super(message);
|
|
648
|
+
this.name = "RelationError";
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
var relationNameFor = (table, key) => `${table}_${columnName(key)}`;
|
|
652
|
+
var AUTH_USER = "User";
|
|
653
|
+
var isAuthUserTarget = (target) => target === AUTH_USER;
|
|
654
|
+
var defaultAuthUserOnDelete = (required) => required ? "restrict" : "set null";
|
|
655
|
+
function relationGraph(resources) {
|
|
656
|
+
const byName = new Map(resources.map((resource) => [resource.name, resource]));
|
|
657
|
+
const graph = { byResource: {}, pending: [] };
|
|
658
|
+
for (const resource of resources) {
|
|
659
|
+
const relations = { belongsTo: [], hasMany: [] };
|
|
660
|
+
for (const [key, def] of storedFields(resource)) {
|
|
661
|
+
if (def.kind !== "belongsTo") continue;
|
|
662
|
+
const authUser = isAuthUserTarget(def.target);
|
|
663
|
+
if (!authUser && !byName.has(def.target)) {
|
|
664
|
+
throw new RelationError(`${resource.name}.${key} belongs to "${def.target}", but there is no ${def.target} resource.`);
|
|
665
|
+
}
|
|
666
|
+
relations.belongsTo.push({
|
|
667
|
+
key,
|
|
668
|
+
name: key.replace(/Id$/, ""),
|
|
669
|
+
target: def.target,
|
|
670
|
+
required: def.required,
|
|
671
|
+
onDelete: def.onDelete ?? (authUser ? defaultAuthUserOnDelete(def.required) : "restrict"),
|
|
672
|
+
relationName: relationNameFor(resource.table, key)
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
graph.byResource[resource.name] = relations;
|
|
676
|
+
}
|
|
677
|
+
for (const resource of resources) {
|
|
678
|
+
for (const [key, def] of Object.entries(resource.fields)) {
|
|
679
|
+
if (def.kind !== "hasMany") continue;
|
|
680
|
+
const target = byName.get(def.target);
|
|
681
|
+
if (!target) {
|
|
682
|
+
graph.pending.push({ resource: resource.name, key, target: def.target });
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const foreignKey = def.foreignKey ?? `${camelCase(resource.name)}Id`;
|
|
686
|
+
const back = graph.byResource[target.name].belongsTo.find((relation) => relation.key === foreignKey);
|
|
687
|
+
if (!back || back.target !== resource.name) {
|
|
688
|
+
throw new RelationError(
|
|
689
|
+
`${resource.name}.${key} is hasMany(${target.name}), but ${target.name} has no ${foreignKey}: belongsTo(${resource.name}) field. Add it to ${target.name}, or set foreignKey to the field that points back.`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
graph.byResource[resource.name].hasMany.push({ key, target: target.name, foreignKey, relationName: back.relationName });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
for (const resource of resources) {
|
|
696
|
+
const { belongsTo, hasMany } = graph.byResource[resource.name];
|
|
697
|
+
const taken = new Set(Object.keys(resource.fields));
|
|
698
|
+
for (const { key, name } of belongsTo) {
|
|
699
|
+
if (taken.has(name)) {
|
|
700
|
+
throw new RelationError(`${resource.name}.${key} exposes relation "${name}", which collides with the ${resource.name}.${name} field. Rename one of them.`);
|
|
701
|
+
}
|
|
702
|
+
taken.add(name);
|
|
703
|
+
}
|
|
704
|
+
for (const { key } of hasMany) {
|
|
705
|
+
if (belongsTo.some((relation) => relation.name === key)) {
|
|
706
|
+
throw new RelationError(`${resource.name}.${key} collides with a belongsTo relation of the same name.`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return graph;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/resource/display.ts
|
|
714
|
+
var TONES = [
|
|
715
|
+
["success", /^(paid|fulfilled|active|approved|completed?|delivered|done|published|resolved|shipped|succeeded|success|verified|won)$/],
|
|
716
|
+
["danger", /^(banned|blocked|cancel+ed|churned|declined|error|expired|failed|lost|overdue|refunded|rejected|suspended)$/],
|
|
717
|
+
["warning", /^(awaiting|draft|in[-_ ]?review|on[-_ ]?hold|pending|processing|review|trial|unpaid|unverified)$/]
|
|
718
|
+
];
|
|
719
|
+
function statusTone(value) {
|
|
720
|
+
if (typeof value !== "string") return "neutral";
|
|
721
|
+
const normalized = value.trim().toLowerCase();
|
|
722
|
+
return TONES.find(([, pattern]) => pattern.test(normalized))?.[0] ?? "neutral";
|
|
723
|
+
}
|
|
724
|
+
function optionLabel(def, value) {
|
|
725
|
+
if ((def.kind === "enum" || def.kind === "multiselect") && def.optionLabels?.[value]) return def.optionLabels[value];
|
|
726
|
+
const text = value.replace(/[_-]+/g, " ").trim();
|
|
727
|
+
return text ? text[0].toUpperCase() + text.slice(1) : value;
|
|
728
|
+
}
|
|
729
|
+
function formatValue(def, value, options = {}) {
|
|
730
|
+
if (value === null || value === void 0 || value === "") return "\u2014";
|
|
731
|
+
const locale = options.locale ?? "en-US";
|
|
732
|
+
switch (def.kind) {
|
|
733
|
+
case "boolean":
|
|
734
|
+
return value ? "Yes" : "No";
|
|
735
|
+
case "int":
|
|
736
|
+
return new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }).format(Number(value));
|
|
737
|
+
case "float":
|
|
738
|
+
return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(Number(value));
|
|
739
|
+
case "date": {
|
|
740
|
+
const date = /* @__PURE__ */ new Date(`${String(value)}T00:00:00Z`);
|
|
741
|
+
return Number.isNaN(date.getTime()) ? String(value) : new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone: "UTC" }).format(date);
|
|
742
|
+
}
|
|
743
|
+
case "datetime":
|
|
744
|
+
case "timestamp": {
|
|
745
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
746
|
+
return Number.isNaN(date.getTime()) ? String(value) : new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short", timeZone: options.timeZone ?? "UTC" }).format(date);
|
|
747
|
+
}
|
|
748
|
+
case "enum":
|
|
749
|
+
return optionLabel(def, String(value));
|
|
750
|
+
case "multiselect":
|
|
751
|
+
return Array.isArray(value) && value.length ? value.map((item) => optionLabel(def, String(item))).join(", ") : "\u2014";
|
|
752
|
+
case "tags":
|
|
753
|
+
return Array.isArray(value) && value.length ? value.map(String).join(", ") : "\u2014";
|
|
754
|
+
case "json": {
|
|
755
|
+
if (value === null || value === void 0) return "\u2014";
|
|
756
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
757
|
+
return text.length > 60 ? `${text.slice(0, 59)}\u2026` : text;
|
|
758
|
+
}
|
|
759
|
+
case "string":
|
|
760
|
+
if (def.format === "tel") return formatPhone(String(value));
|
|
761
|
+
if (def.format === "country") return `${flagEmoji(String(value))} ${countryName(String(value), locale.split("-")[0])}`.trim();
|
|
762
|
+
return String(value);
|
|
763
|
+
case "file":
|
|
764
|
+
return String(value).split("/").pop().replace(/^[0-9a-f-]{36}-/, "");
|
|
765
|
+
default:
|
|
766
|
+
return String(value);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/resource/form.ts
|
|
771
|
+
function initialFormValues(resource, record) {
|
|
772
|
+
const values = {};
|
|
773
|
+
for (const [key, def] of storedFields(resource)) {
|
|
774
|
+
const source = record ? record[key] : "default" in def ? def.default : void 0;
|
|
775
|
+
if (def.kind === "boolean") values[key] = source === true || source === 1;
|
|
776
|
+
else if (def.kind === "multiselect" || def.kind === "tags") values[key] = JSON.stringify(Array.isArray(source) ? source : []);
|
|
777
|
+
else if (def.kind === "json") values[key] = source === null || source === void 0 ? "" : JSON.stringify(source, null, 2);
|
|
778
|
+
else if (source === null || source === void 0) values[key] = "";
|
|
779
|
+
else if (source instanceof Date) values[key] = source.toISOString();
|
|
780
|
+
else values[key] = String(source);
|
|
781
|
+
}
|
|
782
|
+
return values;
|
|
783
|
+
}
|
|
784
|
+
function formValuesToInput(resource, values, mode) {
|
|
785
|
+
const input = {};
|
|
786
|
+
for (const [key, def] of storedFields(resource)) {
|
|
787
|
+
const raw = values[key];
|
|
788
|
+
if (def.kind === "boolean") {
|
|
789
|
+
input[key] = raw === true;
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (def.kind === "multiselect" || def.kind === "tags") {
|
|
793
|
+
const picked = parseMultiValue(raw);
|
|
794
|
+
if (picked.length === 0 && mode === "create" && "default" in def && def.default !== void 0) continue;
|
|
795
|
+
input[key] = picked.length === 0 && !def.required ? null : picked;
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (def.kind === "json") {
|
|
799
|
+
const source = typeof raw === "string" ? raw.trim() : "";
|
|
800
|
+
if (source === "") {
|
|
801
|
+
if (mode === "create" && "default" in def && def.default !== void 0) continue;
|
|
802
|
+
if (!def.required) input[key] = null;
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
input[key] = JSON.parse(source);
|
|
807
|
+
} catch {
|
|
808
|
+
input[key] = Number.NaN;
|
|
809
|
+
}
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
const text = typeof raw === "string" ? raw : "";
|
|
813
|
+
const empty = text.trim() === "";
|
|
814
|
+
if (empty) {
|
|
815
|
+
const hasDefault = "default" in def && def.default !== void 0;
|
|
816
|
+
if (mode === "create" && hasDefault) continue;
|
|
817
|
+
if (!def.required) input[key] = null;
|
|
818
|
+
else if (def.kind === "string" || def.kind === "text") input[key] = text;
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (def.kind === "int" || def.kind === "float") {
|
|
822
|
+
const number = Number(text.trim());
|
|
823
|
+
input[key] = Number.isFinite(number) ? number : text;
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
input[key] = text;
|
|
827
|
+
}
|
|
828
|
+
return input;
|
|
829
|
+
}
|
|
830
|
+
function parseMultiValue(raw) {
|
|
831
|
+
if (typeof raw !== "string" || raw === "") return [];
|
|
832
|
+
try {
|
|
833
|
+
const parsed = JSON.parse(raw);
|
|
834
|
+
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
835
|
+
} catch {
|
|
836
|
+
return [];
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function issuesByField(issues) {
|
|
840
|
+
const errors = {};
|
|
841
|
+
for (const issue of issues) {
|
|
842
|
+
const path = Array.isArray(issue.path) ? issue.path.map(String).join(".") : issue.path;
|
|
843
|
+
const key = path.split(".")[0] || "_form";
|
|
844
|
+
errors[key] ??= issue.message;
|
|
845
|
+
}
|
|
846
|
+
return errors;
|
|
847
|
+
}
|
|
848
|
+
|
|
495
849
|
// src/resource/policy.ts
|
|
496
850
|
var ROLE = /^(\*|[a-z][a-z0-9_-]{0,31})$/;
|
|
497
851
|
var FIELD = /^[a-z][A-Za-z0-9]*$/;
|
|
@@ -581,6 +935,11 @@ export {
|
|
|
581
935
|
defineResource,
|
|
582
936
|
storedFields,
|
|
583
937
|
columnName,
|
|
938
|
+
conditionHolds,
|
|
939
|
+
describeCondition,
|
|
940
|
+
prismaEnumMember,
|
|
941
|
+
enumMemberCollision,
|
|
942
|
+
enumTranslations,
|
|
584
943
|
flagEmoji,
|
|
585
944
|
countryName,
|
|
586
945
|
countries,
|
|
@@ -597,6 +956,18 @@ export {
|
|
|
597
956
|
fieldSchema,
|
|
598
957
|
fileKeyPrefix,
|
|
599
958
|
createValidators,
|
|
959
|
+
RelationError,
|
|
960
|
+
AUTH_USER,
|
|
961
|
+
isAuthUserTarget,
|
|
962
|
+
defaultAuthUserOnDelete,
|
|
963
|
+
relationGraph,
|
|
964
|
+
statusTone,
|
|
965
|
+
optionLabel,
|
|
966
|
+
formatValue,
|
|
967
|
+
initialFormValues,
|
|
968
|
+
formValuesToInput,
|
|
969
|
+
parseMultiValue,
|
|
970
|
+
issuesByField,
|
|
600
971
|
PolicyError,
|
|
601
972
|
definePolicy,
|
|
602
973
|
can,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// src/rate-limit.ts
|
|
2
|
+
var WINDOWS = {
|
|
3
|
+
s: 1,
|
|
4
|
+
sec: 1,
|
|
5
|
+
secs: 1,
|
|
6
|
+
second: 1,
|
|
7
|
+
seconds: 1,
|
|
8
|
+
m: 60,
|
|
9
|
+
min: 60,
|
|
10
|
+
mins: 60,
|
|
11
|
+
minute: 60,
|
|
12
|
+
minutes: 60,
|
|
13
|
+
h: 3600,
|
|
14
|
+
hr: 3600,
|
|
15
|
+
hour: 3600,
|
|
16
|
+
hours: 3600,
|
|
17
|
+
d: 86400,
|
|
18
|
+
day: 86400,
|
|
19
|
+
days: 86400
|
|
20
|
+
};
|
|
21
|
+
var RateSyntaxError = class extends Error {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "RateSyntaxError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
function parseRate(spec) {
|
|
28
|
+
const match = /^\s*(\d+)\s*\/\s*(\d*)\s*([a-z]+)\s*$/i.exec(spec);
|
|
29
|
+
if (!match) {
|
|
30
|
+
throw new RateSyntaxError(`"${spec}" is not a rate. Write it as calls/window, e.g. "10/min", "5/s" or "30/5min".`);
|
|
31
|
+
}
|
|
32
|
+
const limit = Number(match[1]);
|
|
33
|
+
const count = match[2] === "" ? 1 : Number(match[2]);
|
|
34
|
+
const unit = WINDOWS[match[3].toLowerCase()];
|
|
35
|
+
if (unit === void 0) {
|
|
36
|
+
throw new RateSyntaxError(`"${spec}" uses an unknown window "${match[3]}". Use s, min, hour or day.`);
|
|
37
|
+
}
|
|
38
|
+
if (limit < 1) throw new RateSyntaxError(`"${spec}" allows no calls at all. To forbid an action, use a policy instead of a rate limit.`);
|
|
39
|
+
if (count < 1) throw new RateSyntaxError(`"${spec}" has a window of zero.`);
|
|
40
|
+
return { limit, seconds: count * unit };
|
|
41
|
+
}
|
|
42
|
+
function rateFor(config, action) {
|
|
43
|
+
if (!config) return void 0;
|
|
44
|
+
const spec = config[action] ?? config["*"];
|
|
45
|
+
return spec === void 0 ? void 0 : parseRate(spec);
|
|
46
|
+
}
|
|
47
|
+
function ratesIn(config) {
|
|
48
|
+
if (!config) return [];
|
|
49
|
+
return Object.entries(config).filter(([, spec]) => typeof spec === "string").map(([action, spec]) => ({ action, spec, rate: parseRate(spec) }));
|
|
50
|
+
}
|
|
51
|
+
var rateLimitKey = (resource, action, caller) => `${resource}:${action}:${caller}`;
|
|
52
|
+
async function fixedWindow(rate, increment) {
|
|
53
|
+
const { count, ttl } = await increment(rate.seconds);
|
|
54
|
+
const remaining = Math.max(0, rate.limit - count);
|
|
55
|
+
return { allowed: count <= rate.limit, remaining, retryAfter: Math.max(1, Math.ceil(ttl)) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export {
|
|
59
|
+
RateSyntaxError,
|
|
60
|
+
parseRate,
|
|
61
|
+
rateFor,
|
|
62
|
+
ratesIn,
|
|
63
|
+
rateLimitKey,
|
|
64
|
+
fixedWindow
|
|
65
|
+
};
|