@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.
@@ -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 = { person: "people", man: "men", woman: "women", child: "children", mouse: "mice" };
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
- const match = /([A-Za-z]+)$/.exec(value);
167
- if (!match) return value;
168
- const word = match[1];
169
- const lower = word.toLowerCase();
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
- var isIpAddress = (value) => /^(?:\d{1,3}\.){3}\d{1,3}$/.test(value) ? value.split(".").every((part) => Number(part) <= 255) : /^[0-9a-f:]+$/i.test(value) && value.includes(":") && value.split("::").length <= 2;
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 value = def.required ? base : base.nullable();
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
+ };