@flaredev/core 0.11.0 → 0.12.1

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/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { F as Field, C as CreateInput, U as UpdateInput, R as Resource, S as StoredField } from './policy-DaK3dWQ_.js';
2
- export { B as BelongsToField, a as BooleanField, b as ClientResource, D as DEFAULT_FILE_MAX_BYTES, c as DateField, d as DateTimeField, E as EnumField, e as FILE_CATEGORIES, f as FieldCondition, g as FieldKind, h as FieldValue, i as FileCategory, j as FileField, k as FloatField, H as HasManyField, I as IntField, J as JsonField, l as JsonOptions, M as MultiSelectField, N as NUMBER_FORMATS, m as NumberFormat, P as Policy, n as PolicyAction, o as PolicyConfig, p as PolicyError, q as PolicyOwnership, r as RESERVED_FIELD_NAMES, s as RecordOf, t as ResourceConfig, u as ResourceDefinitionError, v as STRING_FORMATS, w as StringField, x as StringFormat, T as TEXT_FORMATS, y as TagsField, z as TagsOptions, A as TextField, G as TextFormat, K as allowedActions, L as can, O as clientResource, Q as columnName, V as conditionHolds, W as defaultFieldLabel, X as definePolicy, Y as defineResource, Z as describeCondition, _ as field, $ as fileMaxBytes, a0 as mimeTypesFor, a1 as ownershipFilter, a2 as storedFields } from './policy-DaK3dWQ_.js';
1
+ export { R as Rate, b as RateLimitConfig, a as RateLimitDecision, c as RateLimitStore, d as RateLimitedAction, e as RateSyntaxError, f as fixedWindow, p as parseRate, r as rateFor, g as rateLimitKey, h as ratesIn } from './rate-limit-ClbdZioJ.js';
2
+ import { F as Field, C as CreateInput, U as UpdateInput, R as Resource, S as StoredField } from './policy-b4FXN3ZJ.js';
3
+ export { B as BelongsToField, f as BooleanField, g as ClientResource, D as DEFAULT_FILE_MAX_BYTES, h as DateField, i as DateTimeField, E as EnumField, j as FILE_CATEGORIES, k as FieldCondition, l as FieldKind, m as FieldValue, n as FileCategory, o as FileField, q as FloatField, H as HasManyField, I as ISSUE_CODES, r as IntField, c as IssueCode, J as JsonField, s as JsonOptions, M as MultiSelectField, N as NUMBER_FORMATS, t as NumberFormat, d as PROBLEM_CODES, b as Policy, u as PolicyAction, v as PolicyConfig, x as PolicyError, y as PolicyOwnership, e as ProblemBody, P as ProblemCode, a as ProblemIssue, z as RESERVED_FIELD_NAMES, A as RecordOf, G as ResourceConfig, K as ResourceDefinitionError, L as STRING_FORMATS, O as StringField, Q as StringFormat, T as TEXT_FORMATS, V as TagsField, W as TagsOptions, X as TextField, Y as TextFormat, Z as allowedActions, _ as can, $ as clientResource, a0 as columnName, a1 as conditionHolds, a2 as defaultFieldLabel, a3 as definePolicy, a4 as defineResource, a5 as describeCondition, a6 as field, a7 as fileMaxBytes, a8 as mimeTypesFor, a9 as ownershipFilter, p as problemResponse, aa as storedFields, w as wantsJson } from './policy-b4FXN3ZJ.js';
3
4
  import { z } from 'zod';
4
5
 
5
6
  /**
@@ -141,6 +142,19 @@ interface StorageBucket {
141
142
  truncated: boolean;
142
143
  cursor?: string;
143
144
  }>;
145
+ /**
146
+ * A URL that serves this object directly, for a provider that is already a CDN.
147
+ *
148
+ * Returning one makes `createReadUrl` hand it back instead of a signed, expiring URL
149
+ * through this app — which is the point of Cloudinary or UploadThing, and the reason
150
+ * to pay for them. It also means the file is readable by anyone who has the link, for
151
+ * as long as it exists: there is no token to expire and no policy consulted on the way
152
+ * in. Buckets that hold anything private leave this undefined, and R2 and disk do.
153
+ *
154
+ * Return `undefined` for a key this bucket cannot address publicly, and the signed
155
+ * path is used for it instead.
156
+ */
157
+ publicUrl?(key: string): string | undefined;
144
158
  }
145
159
  interface StorageObjectBody {
146
160
  body: ReadableStream;
@@ -187,7 +201,13 @@ declare function createStorage(options: StorageOptions): {
187
201
  key: string;
188
202
  expiresAt: Date;
189
203
  }>;
190
- /** A temporary URL that serves the object. */
204
+ /**
205
+ * A temporary URL that serves the object.
206
+ *
207
+ * On a bucket that is its own CDN, this is that provider's URL and is neither
208
+ * temporary nor private — see {@link StorageBucket.publicUrl}. `downloadAs` still
209
+ * takes the signed path, because forcing a filename is this app's header to set.
210
+ */
191
211
  createReadUrl(read: ReadUrlOptions): Promise<string>;
192
212
  delete: (key: string) => Promise<void>;
193
213
  /** Handles `PUT <routePath>?token=` (upload) and `GET <routePath>?token=` (read). */
@@ -331,6 +351,79 @@ declare function fromMinorUnits(minor: number, currency?: string): number;
331
351
  */
332
352
  declare function hasTooManyPlaces(amount: number, currency?: string): boolean;
333
353
 
354
+ /**
355
+ * Turning an enum option into a Prisma enum member name, and back.
356
+ *
357
+ * In core rather than in the CLI because three places need the same rule and they must
358
+ * not drift: the Prisma schema generator writes the members, the seed writer writes
359
+ * values through them, and the app's own Prisma store has to translate in both
360
+ * directions at runtime.
361
+ *
362
+ * A Prisma enum member has to be an identifier, so an option that is not one is renamed
363
+ * and the real value is kept with `@map`. That mapping applies to the database column,
364
+ * not to the client: Prisma's generated client speaks member names, so `A+` stored in
365
+ * Postgres arrives in JavaScript as `A_2b`. Translating at the store boundary is what
366
+ * keeps that out of the rest of the app — the point of the exercise is that the user
367
+ * sees `A+`.
368
+ */
369
+ /**
370
+ * The identifier a Prisma enum member gets. `in-progress` can't be one, so it becomes
371
+ * `in_progress`, and `A+` becomes `A_2b`, each with the real value in `@map`.
372
+ *
373
+ * Anything going through the Prisma client — `createMany` in a seed, a `where` clause —
374
+ * has to use this name rather than the value stored in the column, so it is exported
375
+ * and shared rather than written twice.
376
+ *
377
+ * `-` and a space become `_` rather than `_2d` and `_20`. That is not the tidiest rule,
378
+ * but it is the right one: both were reachable long before the rest of the punctuation
379
+ * was — `-` through the field grammar, a space through a hand-written descriptor — and
380
+ * escaping them now would quietly rename `in_progress` in every schema already
381
+ * generated, breaking every `where` clause written against it.
382
+ *
383
+ * Everything else is escaped by code point, which is what stops two options flattening
384
+ * onto one member: `A+` and `A-` both became `A_` before this, and a Prisma schema that
385
+ * declares the same member twice does not generate at all. The separators can still
386
+ * collide with each other (`in progress` against `in-progress`), so callers check with
387
+ * `enumMemberCollision` rather than trusting this to be injective.
388
+ */
389
+ declare function prismaEnumMember(option: string): string;
390
+ /**
391
+ * The first pair of options that would end up as the same member name, if there is one.
392
+ *
393
+ * Escaping by code point rules out the collisions that come up in practice, but not all
394
+ * of them: `in progress` and `in-progress` both fold to `in_progress`, an option spelled
395
+ * literally `A_2b` collides with `A+`, and `A_` with `A-`. Rare, and worth catching
396
+ * anyway — the alternative is a Prisma schema that declares one member twice and fails to
397
+ * generate, with an error that says nothing about which two options caused it.
398
+ */
399
+ declare function enumMemberCollision(options: readonly string[]): {
400
+ a: string;
401
+ b: string;
402
+ member: string;
403
+ } | undefined;
404
+ /**
405
+ * The two lookups a store needs to translate a resource's enum values, by field.
406
+ *
407
+ * Built once per resource rather than per row. `toMember` is used on the way in — to
408
+ * Prisma — and `toValue` on the way out; a field whose options are all identifiers
409
+ * already is left out of both, so the common case costs nothing.
410
+ */
411
+ interface EnumTranslation {
412
+ toMember: Record<string, string>;
413
+ toValue: Record<string, string>;
414
+ }
415
+ /**
416
+ * Translation tables for every enum field whose options are not already identifiers.
417
+ *
418
+ * `fields` is the resource's stored fields, as `storedFields()` gives them. Taking that
419
+ * rather than a `Resource` keeps this usable from a store that was handed a descriptor
420
+ * and from the generator, which works field by field.
421
+ */
422
+ declare function enumTranslations(fields: Iterable<readonly [string, {
423
+ kind: string;
424
+ options?: readonly string[];
425
+ }]>): Record<string, EnumTranslation>;
426
+
334
427
  /**
335
428
  * Zod schemas derived from a descriptor at runtime. Unknown keys are rejected,
336
429
  * so clients can't set `id`, timestamps or undeclared columns (no mass assignment).
@@ -396,6 +489,29 @@ interface RelationGraph {
396
489
  declare class RelationError extends Error {
397
490
  constructor(message: string);
398
491
  }
492
+ /**
493
+ * The one relation target that is not a generated resource.
494
+ *
495
+ * Better Auth owns the account table — `user` in Drizzle, `User` in Prisma — and
496
+ * generating a `User` resource to point at would create a second, competing user table.
497
+ * So `belongsTo("User")` resolves to the auth table instead, which is what makes "who
498
+ * reported it" a relation like any other rather than a bare string column.
499
+ */
500
+ declare const AUTH_USER = "User";
501
+ /** Whether a belongsTo target is Better Auth's account table rather than a resource. */
502
+ declare const isAuthUserTarget: (target: string) => target is "User";
503
+ /**
504
+ * What `onDelete` a relation to the auth user gets when the descriptor doesn't say.
505
+ *
506
+ * `set null` when the field is optional, which is the case worth defaulting for: a
507
+ * person who closes their account should not be held open by a lost-property report
508
+ * from two years ago, and the report itself usually wants to outlive them.
509
+ *
510
+ * `restrict` when the field is required, because there is nothing else it could be — a
511
+ * required column cannot be set to null, and silently deleting the record instead
512
+ * (`cascade`) would destroy data the user never asked to lose.
513
+ */
514
+ declare const defaultAuthUserOnDelete: (required: boolean) => "restrict" | "set null";
399
515
  /**
400
516
  * Resolve belongsTo/hasMany across all resources. Throws for belongsTo targets that
401
517
  * don't exist and for hasMany declarations without a matching belongsTo on the target.
@@ -626,6 +742,6 @@ type SeedUser = (user: SeedUserInput) => Promise<CredentialUserRows["user"]>;
626
742
  type Seed = (context: SeedContext) => void | Promise<void>;
627
743
  declare function defineSeed(seed: Seed): Seed;
628
744
 
629
- declare const FLARE_VERSION = "0.11.0";
745
+ declare const FLARE_VERSION = "0.12.1";
630
746
 
631
- export { type BelongsToRelation, type Country, CreateInput, type CredentialUserRows, DEFAULT_MONEY_EXPONENT, FLARE_VERSION, type Fake, Field, type FormValues, type FormatOptions, type HasManyRelation, type InsertMany, type MailMessage, type MailResult, type Mailer, type MailerOptions, PBKDF2_ITERATIONS, type ReadUrlOptions, RelationError, type RelationGraph, Resource, type ResourceRelations, type ResourceValidators, SNIFF_BYTES, SUBSCRIPTION_STATUSES, type Seed, type SeedContext, type SeedUser, type SeedUserInput, type Storage, type StorageBucket, type StorageOptions, StoredField, type SubscriptionLike, type SubscriptionState, type SubscriptionStatus, type Tone, type TransactionalEmail, UpdateInput, type UploadUrlOptions, camelCase, checkoutPaid, countries, countryName, createFake, createMailer, createObjectKey, createStorage, createValidators, credentialUserRows, currentPeriodEnd, defineSeed, fake, fieldSchema, fileKeyPrefix, flagEmoji, formValuesToInput, formatPhone, formatValue, fromMinorUnits, grantsAccess, hasTooManyPlaces, hashPassword, humanize, importSigningKey, initialFormValues, isColor, isCountryCode, isDomain, isLiveSubscription, isPhoneNumber, isSlug, issuesByField, kebabCase, matchesContentType, moneyExponent, normalizeDomain, optionLabel, parseMultiValue, pascalCase, pluralize, randomLetters, relationGraph, renderTransactionalEmail, shouldApplySubscription, signToken, slugify, snakeCase, sniffMatches, splitPhone, statusTone, subscriptionPriceId, subscriptionState, toE164, toMinorUnits, toSubscriptionStatus, verifyPassword, verifyToken, words };
747
+ export { AUTH_USER, type BelongsToRelation, type Country, CreateInput, type CredentialUserRows, DEFAULT_MONEY_EXPONENT, type EnumTranslation, FLARE_VERSION, type Fake, Field, type FormValues, type FormatOptions, type HasManyRelation, type InsertMany, type MailMessage, type MailResult, type Mailer, type MailerOptions, PBKDF2_ITERATIONS, type ReadUrlOptions, RelationError, type RelationGraph, Resource, type ResourceRelations, type ResourceValidators, SNIFF_BYTES, SUBSCRIPTION_STATUSES, type Seed, type SeedContext, type SeedUser, type SeedUserInput, type Storage, type StorageBucket, type StorageObjectBody, type StorageOptions, StoredField, type SubscriptionLike, type SubscriptionState, type SubscriptionStatus, type Tone, type TransactionalEmail, UpdateInput, type UploadUrlOptions, camelCase, checkoutPaid, countries, countryName, createFake, createMailer, createObjectKey, createStorage, createValidators, credentialUserRows, currentPeriodEnd, defaultAuthUserOnDelete, defineSeed, enumMemberCollision, enumTranslations, fake, fieldSchema, fileKeyPrefix, flagEmoji, formValuesToInput, formatPhone, formatValue, fromMinorUnits, grantsAccess, hasTooManyPlaces, hashPassword, humanize, importSigningKey, initialFormValues, isAuthUserTarget, isColor, isCountryCode, isDomain, isLiveSubscription, isPhoneNumber, isSlug, issuesByField, kebabCase, matchesContentType, moneyExponent, normalizeDomain, optionLabel, parseMultiValue, pascalCase, pluralize, prismaEnumMember, randomLetters, relationGraph, renderTransactionalEmail, shouldApplySubscription, signToken, slugify, snakeCase, sniffMatches, splitPhone, statusTone, subscriptionPriceId, subscriptionState, toE164, toMinorUnits, toSubscriptionStatus, verifyPassword, verifyToken, words };
package/dist/index.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import {
2
+ AUTH_USER,
2
3
  DEFAULT_FILE_MAX_BYTES,
3
4
  DEFAULT_MONEY_EXPONENT,
4
5
  FILE_CATEGORIES,
5
6
  NUMBER_FORMATS,
6
7
  PolicyError,
7
8
  RESERVED_FIELD_NAMES,
9
+ RelationError,
8
10
  ResourceDefinitionError,
9
11
  STRING_FORMATS,
10
12
  TEXT_FORMATS,
@@ -17,39 +19,66 @@ import {
17
19
  countries,
18
20
  countryName,
19
21
  createValidators,
22
+ defaultAuthUserOnDelete,
20
23
  defaultFieldLabel,
21
24
  definePolicy,
22
25
  defineResource,
23
26
  describeCondition,
27
+ enumMemberCollision,
28
+ enumTranslations,
24
29
  field,
25
30
  fieldSchema,
26
31
  fileKeyPrefix,
27
32
  fileMaxBytes,
28
33
  flagEmoji,
34
+ formValuesToInput,
29
35
  formatPhone,
36
+ formatValue,
30
37
  fromMinorUnits,
31
38
  hasTooManyPlaces,
32
39
  humanize,
40
+ initialFormValues,
41
+ isAuthUserTarget,
33
42
  isColor,
34
43
  isCountryCode,
35
44
  isDomain,
36
45
  isPhoneNumber,
37
46
  isSlug,
47
+ issuesByField,
38
48
  kebabCase,
39
49
  mimeTypesFor,
40
50
  moneyExponent,
41
51
  normalizeDomain,
52
+ optionLabel,
42
53
  ownershipFilter,
54
+ parseMultiValue,
43
55
  pascalCase,
44
56
  pluralize,
57
+ prismaEnumMember,
58
+ relationGraph,
45
59
  slugify,
46
60
  snakeCase,
47
61
  splitPhone,
62
+ statusTone,
48
63
  storedFields,
49
64
  toE164,
50
65
  toMinorUnits,
51
66
  words
52
- } from "./chunk-62JI5DCR.js";
67
+ } from "./chunk-BTVEH5AU.js";
68
+ import {
69
+ ISSUE_CODES,
70
+ PROBLEM_CODES,
71
+ problemResponse,
72
+ wantsJson
73
+ } from "./chunk-7NQIVWGG.js";
74
+ import {
75
+ RateSyntaxError,
76
+ fixedWindow,
77
+ parseRate,
78
+ rateFor,
79
+ rateLimitKey,
80
+ ratesIn
81
+ } from "./chunk-I2HSIPCF.js";
53
82
 
54
83
  // src/password.ts
55
84
  var ALGORITHM = "pbkdf2-sha256";
@@ -369,8 +398,8 @@ function replay(chunks, reader, length) {
369
398
  });
370
399
  return fixed.readable;
371
400
  }
372
- function error(status, message) {
373
- return Response.json({ error: message }, { status });
401
+ function error(status, message, code) {
402
+ return problemResponse(status, message, code);
374
403
  }
375
404
  function createStorage(options) {
376
405
  const routePath = options.routePath ?? "/api/storage";
@@ -381,36 +410,36 @@ function createStorage(options) {
381
410
  const urlFor = (token) => `${options.baseURL?.replace(/\/+$/, "") ?? ""}${routePath}?token=${token}`;
382
411
  async function handle(request) {
383
412
  const token = new URL(request.url).searchParams.get("token");
384
- if (!token) return error(400, "Missing token.");
413
+ if (!token) return error(400, "Missing token.", PROBLEM_CODES.invalidToken);
385
414
  const payload = await verifyToken(await signingKey(), token);
386
415
  if (!payload || typeof payload.exp !== "number" || !isValidKey(payload.key)) {
387
- return error(403, "Invalid token.");
416
+ return error(403, "Invalid token.", PROBLEM_CODES.invalidToken);
388
417
  }
389
- if (payload.exp < Date.now() / 1e3) return error(403, "Token expired.");
418
+ if (payload.exp < Date.now() / 1e3) return error(403, "Token expired.", PROBLEM_CODES.invalidToken);
390
419
  if (request.method === "PUT") {
391
- if (payload.op !== "put") return error(403, "Token does not allow uploads.");
420
+ if (payload.op !== "put") return error(403, "Token does not allow uploads.", PROBLEM_CODES.invalidToken);
392
421
  const contentType = request.headers.get("content-type") ?? "";
393
422
  if (!matchesContentType(contentType, payload.types)) {
394
- return error(415, `Content-Type must be one of: ${payload.types.join(", ")}.`);
423
+ return error(415, `Content-Type must be one of: ${payload.types.join(", ")}.`, PROBLEM_CODES.unsupportedMediaType);
395
424
  }
396
425
  const length = Number(request.headers.get("content-length"));
397
426
  if (!request.headers.has("content-length") || !Number.isInteger(length) || length < 0) {
398
- return error(411, "Content-Length is required.");
427
+ return error(411, "Content-Length is required.", PROBLEM_CODES.lengthRequired);
399
428
  }
400
- if (length > payload.max) return error(413, `File exceeds the ${payload.max}-byte limit.`);
401
- if (!request.body) return error(400, "Missing request body.");
429
+ if (length > payload.max) return error(413, `File exceeds the ${payload.max}-byte limit.`, PROBLEM_CODES.payloadTooLarge);
430
+ if (!request.body) return error(400, "Missing request body.", PROBLEM_CODES.badRequest);
402
431
  const { head, chunks, reader } = await readHead(request.body, SNIFF_BYTES);
403
432
  if (sniffMatches(contentType, head) === false) {
404
433
  await drain(reader);
405
- return error(415, `The file's contents don't match its type (${contentType.split(";")[0].trim()}).`);
434
+ return error(415, `The file's contents don't match its type (${contentType.split(";")[0].trim()}).`, PROBLEM_CODES.unsupportedMediaType);
406
435
  }
407
436
  await options.bucket.put(payload.key, replay(chunks, reader, length), { httpMetadata: { contentType } });
408
437
  return Response.json({ key: payload.key }, { status: 201 });
409
438
  }
410
439
  if (request.method === "GET" || request.method === "HEAD") {
411
- if (payload.op !== "get") return error(403, "Token does not allow reads.");
440
+ if (payload.op !== "get") return error(403, "Token does not allow reads.", PROBLEM_CODES.invalidToken);
412
441
  const object = await options.bucket.get(payload.key);
413
- if (!object) return error(404, "Not found.");
442
+ if (!object) return error(404, "Not found.", PROBLEM_CODES.notFound);
414
443
  const headers = new Headers({
415
444
  "content-type": object.httpMetadata?.contentType ?? "application/octet-stream",
416
445
  "content-length": String(object.size),
@@ -447,9 +476,19 @@ function createStorage(options) {
447
476
  });
448
477
  return { url: urlFor(token), method: "PUT", key: upload.key, expiresAt: new Date(exp * 1e3) };
449
478
  },
450
- /** A temporary URL that serves the object. */
479
+ /**
480
+ * A temporary URL that serves the object.
481
+ *
482
+ * On a bucket that is its own CDN, this is that provider's URL and is neither
483
+ * temporary nor private — see {@link StorageBucket.publicUrl}. `downloadAs` still
484
+ * takes the signed path, because forcing a filename is this app's header to set.
485
+ */
451
486
  async createReadUrl(read) {
452
487
  if (!isValidKey(read.key)) throw new Error(`Invalid object key: ${read.key}`);
488
+ if (!read.downloadAs) {
489
+ const direct = options.bucket.publicUrl?.(read.key);
490
+ if (direct) return direct;
491
+ }
453
492
  const payload = { op: "get", key: read.key, exp: expiry(read.expiresIn) };
454
493
  if (read.downloadAs) payload.name = read.downloadAs;
455
494
  return urlFor(await signToken(await signingKey(), payload));
@@ -510,207 +549,6 @@ function randomLetters(length = 8) {
510
549
  return Array.from(bytes, (byte) => String.fromCharCode(97 + byte % 26)).join("");
511
550
  }
512
551
 
513
- // src/resource/relations.ts
514
- var RelationError = class extends Error {
515
- constructor(message) {
516
- super(message);
517
- this.name = "RelationError";
518
- }
519
- };
520
- var relationNameFor = (table, key) => `${table}_${columnName(key)}`;
521
- function relationGraph(resources) {
522
- const byName = new Map(resources.map((resource) => [resource.name, resource]));
523
- const graph = { byResource: {}, pending: [] };
524
- for (const resource of resources) {
525
- const relations = { belongsTo: [], hasMany: [] };
526
- for (const [key, def] of storedFields(resource)) {
527
- if (def.kind !== "belongsTo") continue;
528
- if (!byName.has(def.target)) {
529
- throw new RelationError(`${resource.name}.${key} belongs to "${def.target}", but there is no ${def.target} resource.`);
530
- }
531
- relations.belongsTo.push({
532
- key,
533
- name: key.replace(/Id$/, ""),
534
- target: def.target,
535
- required: def.required,
536
- onDelete: def.onDelete ?? "restrict",
537
- relationName: relationNameFor(resource.table, key)
538
- });
539
- }
540
- graph.byResource[resource.name] = relations;
541
- }
542
- for (const resource of resources) {
543
- for (const [key, def] of Object.entries(resource.fields)) {
544
- if (def.kind !== "hasMany") continue;
545
- const target = byName.get(def.target);
546
- if (!target) {
547
- graph.pending.push({ resource: resource.name, key, target: def.target });
548
- continue;
549
- }
550
- const foreignKey = def.foreignKey ?? `${camelCase(resource.name)}Id`;
551
- const back = graph.byResource[target.name].belongsTo.find((relation) => relation.key === foreignKey);
552
- if (!back || back.target !== resource.name) {
553
- throw new RelationError(
554
- `${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.`
555
- );
556
- }
557
- graph.byResource[resource.name].hasMany.push({ key, target: target.name, foreignKey, relationName: back.relationName });
558
- }
559
- }
560
- for (const resource of resources) {
561
- const { belongsTo, hasMany } = graph.byResource[resource.name];
562
- const taken = new Set(Object.keys(resource.fields));
563
- for (const { key, name } of belongsTo) {
564
- if (taken.has(name)) {
565
- throw new RelationError(`${resource.name}.${key} exposes relation "${name}", which collides with the ${resource.name}.${name} field. Rename one of them.`);
566
- }
567
- taken.add(name);
568
- }
569
- for (const { key } of hasMany) {
570
- if (belongsTo.some((relation) => relation.name === key)) {
571
- throw new RelationError(`${resource.name}.${key} collides with a belongsTo relation of the same name.`);
572
- }
573
- }
574
- }
575
- return graph;
576
- }
577
-
578
- // src/resource/display.ts
579
- var TONES = [
580
- ["success", /^(paid|fulfilled|active|approved|completed?|delivered|done|published|resolved|shipped|succeeded|success|verified|won)$/],
581
- ["danger", /^(banned|blocked|cancel+ed|churned|declined|error|expired|failed|lost|overdue|refunded|rejected|suspended)$/],
582
- ["warning", /^(awaiting|draft|in[-_ ]?review|on[-_ ]?hold|pending|processing|review|trial|unpaid|unverified)$/]
583
- ];
584
- function statusTone(value) {
585
- if (typeof value !== "string") return "neutral";
586
- const normalized = value.trim().toLowerCase();
587
- return TONES.find(([, pattern]) => pattern.test(normalized))?.[0] ?? "neutral";
588
- }
589
- function optionLabel(def, value) {
590
- if ((def.kind === "enum" || def.kind === "multiselect") && def.optionLabels?.[value]) return def.optionLabels[value];
591
- const text2 = value.replace(/[_-]+/g, " ").trim();
592
- return text2 ? text2[0].toUpperCase() + text2.slice(1) : value;
593
- }
594
- function formatValue(def, value, options = {}) {
595
- if (value === null || value === void 0 || value === "") return "\u2014";
596
- const locale = options.locale ?? "en-US";
597
- switch (def.kind) {
598
- case "boolean":
599
- return value ? "Yes" : "No";
600
- case "int":
601
- return new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }).format(Number(value));
602
- case "float":
603
- return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(Number(value));
604
- case "date": {
605
- const date = /* @__PURE__ */ new Date(`${String(value)}T00:00:00Z`);
606
- return Number.isNaN(date.getTime()) ? String(value) : new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone: "UTC" }).format(date);
607
- }
608
- case "datetime":
609
- case "timestamp": {
610
- const date = value instanceof Date ? value : new Date(value);
611
- return Number.isNaN(date.getTime()) ? String(value) : new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short", timeZone: options.timeZone ?? "UTC" }).format(date);
612
- }
613
- case "enum":
614
- return optionLabel(def, String(value));
615
- case "multiselect":
616
- return Array.isArray(value) && value.length ? value.map((item) => optionLabel(def, String(item))).join(", ") : "\u2014";
617
- case "tags":
618
- return Array.isArray(value) && value.length ? value.map(String).join(", ") : "\u2014";
619
- case "json": {
620
- if (value === null || value === void 0) return "\u2014";
621
- const text2 = typeof value === "string" ? value : JSON.stringify(value);
622
- return text2.length > 60 ? `${text2.slice(0, 59)}\u2026` : text2;
623
- }
624
- case "string":
625
- if (def.format === "tel") return formatPhone(String(value));
626
- if (def.format === "country") return `${flagEmoji(String(value))} ${countryName(String(value), locale.split("-")[0])}`.trim();
627
- return String(value);
628
- case "file":
629
- return String(value).split("/").pop().replace(/^[0-9a-f-]{36}-/, "");
630
- default:
631
- return String(value);
632
- }
633
- }
634
-
635
- // src/resource/form.ts
636
- function initialFormValues(resource, record) {
637
- const values = {};
638
- for (const [key, def] of storedFields(resource)) {
639
- const source = record ? record[key] : "default" in def ? def.default : void 0;
640
- if (def.kind === "boolean") values[key] = source === true || source === 1;
641
- else if (def.kind === "multiselect" || def.kind === "tags") values[key] = JSON.stringify(Array.isArray(source) ? source : []);
642
- else if (def.kind === "json") values[key] = source === null || source === void 0 ? "" : JSON.stringify(source, null, 2);
643
- else if (source === null || source === void 0) values[key] = "";
644
- else if (source instanceof Date) values[key] = source.toISOString();
645
- else values[key] = String(source);
646
- }
647
- return values;
648
- }
649
- function formValuesToInput(resource, values, mode) {
650
- const input = {};
651
- for (const [key, def] of storedFields(resource)) {
652
- const raw = values[key];
653
- if (def.kind === "boolean") {
654
- input[key] = raw === true;
655
- continue;
656
- }
657
- if (def.kind === "multiselect" || def.kind === "tags") {
658
- const picked = parseMultiValue(raw);
659
- if (picked.length === 0 && mode === "create" && "default" in def && def.default !== void 0) continue;
660
- input[key] = picked.length === 0 && !def.required ? null : picked;
661
- continue;
662
- }
663
- if (def.kind === "json") {
664
- const source = typeof raw === "string" ? raw.trim() : "";
665
- if (source === "") {
666
- if (mode === "create" && "default" in def && def.default !== void 0) continue;
667
- if (!def.required) input[key] = null;
668
- continue;
669
- }
670
- try {
671
- input[key] = JSON.parse(source);
672
- } catch {
673
- input[key] = Number.NaN;
674
- }
675
- continue;
676
- }
677
- const text2 = typeof raw === "string" ? raw : "";
678
- const empty = text2.trim() === "";
679
- if (empty) {
680
- const hasDefault = "default" in def && def.default !== void 0;
681
- if (mode === "create" && hasDefault) continue;
682
- if (!def.required) input[key] = null;
683
- else if (def.kind === "string" || def.kind === "text") input[key] = text2;
684
- continue;
685
- }
686
- if (def.kind === "int" || def.kind === "float") {
687
- const number = Number(text2.trim());
688
- input[key] = Number.isFinite(number) ? number : text2;
689
- continue;
690
- }
691
- input[key] = text2;
692
- }
693
- return input;
694
- }
695
- function parseMultiValue(raw) {
696
- if (typeof raw !== "string" || raw === "") return [];
697
- try {
698
- const parsed = JSON.parse(raw);
699
- return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
700
- } catch {
701
- return [];
702
- }
703
- }
704
- function issuesByField(issues) {
705
- const errors = {};
706
- for (const issue of issues) {
707
- const path = Array.isArray(issue.path) ? issue.path.map(String).join(".") : issue.path;
708
- const key = path.split(".")[0] || "_form";
709
- errors[key] ??= issue.message;
710
- }
711
- return errors;
712
- }
713
-
714
552
  // src/seed.ts
715
553
  async function credentialUserRows(input) {
716
554
  const email = input.email.trim().toLowerCase();
@@ -1054,16 +892,20 @@ function createFake(seed = 20260101) {
1054
892
  var fake = createFake();
1055
893
 
1056
894
  // src/index.ts
1057
- var FLARE_VERSION = "0.11.0";
895
+ var FLARE_VERSION = "0.12.1";
1058
896
  export {
897
+ AUTH_USER,
1059
898
  DEFAULT_FILE_MAX_BYTES,
1060
899
  DEFAULT_MONEY_EXPONENT,
1061
900
  FILE_CATEGORIES,
1062
901
  FLARE_VERSION,
902
+ ISSUE_CODES,
1063
903
  NUMBER_FORMATS,
1064
904
  PBKDF2_ITERATIONS,
905
+ PROBLEM_CODES,
1065
906
  PolicyError,
1066
907
  RESERVED_FIELD_NAMES,
908
+ RateSyntaxError,
1067
909
  RelationError,
1068
910
  ResourceDefinitionError,
1069
911
  SNIFF_BYTES,
@@ -1086,16 +928,20 @@ export {
1086
928
  createValidators,
1087
929
  credentialUserRows,
1088
930
  currentPeriodEnd,
931
+ defaultAuthUserOnDelete,
1089
932
  defaultFieldLabel,
1090
933
  definePolicy,
1091
934
  defineResource,
1092
935
  defineSeed,
1093
936
  describeCondition,
937
+ enumMemberCollision,
938
+ enumTranslations,
1094
939
  fake,
1095
940
  field,
1096
941
  fieldSchema,
1097
942
  fileKeyPrefix,
1098
943
  fileMaxBytes,
944
+ fixedWindow,
1099
945
  flagEmoji,
1100
946
  formValuesToInput,
1101
947
  formatPhone,
@@ -1107,6 +953,7 @@ export {
1107
953
  humanize,
1108
954
  importSigningKey,
1109
955
  initialFormValues,
956
+ isAuthUserTarget,
1110
957
  isColor,
1111
958
  isCountryCode,
1112
959
  isDomain,
@@ -1122,9 +969,15 @@ export {
1122
969
  optionLabel,
1123
970
  ownershipFilter,
1124
971
  parseMultiValue,
972
+ parseRate,
1125
973
  pascalCase,
1126
974
  pluralize,
975
+ prismaEnumMember,
976
+ problemResponse,
1127
977
  randomLetters,
978
+ rateFor,
979
+ rateLimitKey,
980
+ ratesIn,
1128
981
  relationGraph,
1129
982
  renderTransactionalEmail,
1130
983
  shouldApplySubscription,
@@ -1142,5 +995,6 @@ export {
1142
995
  toSubscriptionStatus,
1143
996
  verifyPassword,
1144
997
  verifyToken,
998
+ wantsJson,
1145
999
  words
1146
1000
  };