@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/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-DFimkxdx.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 FieldKind, g as FieldValue, h as FileCategory, i as FileField, j as FloatField, H as HasManyField, I as IntField, J as JsonField, k as JsonOptions, M as MultiSelectField, N as NUMBER_FORMATS, l as NumberFormat, P as Policy, m as PolicyAction, n as PolicyConfig, o as PolicyError, p as PolicyOwnership, q as RESERVED_FIELD_NAMES, r as RecordOf, s as ResourceConfig, t as ResourceDefinitionError, u as STRING_FORMATS, v as StringField, w as StringFormat, T as TEXT_FORMATS, x as TagsField, y as TagsOptions, z as TextField, A as TextFormat, G as allowedActions, K as can, L as clientResource, O as columnName, Q as defaultFieldLabel, V as definePolicy, W as defineResource, X as field, Y as fileMaxBytes, Z as mimeTypesFor, _ as ownershipFilter, $ as storedFields } from './policy-DFimkxdx.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.10.0";
745
+ declare const FLARE_VERSION = "0.12.0";
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,
@@ -13,41 +15,70 @@ import {
13
15
  can,
14
16
  clientResource,
15
17
  columnName,
18
+ conditionHolds,
16
19
  countries,
17
20
  countryName,
18
21
  createValidators,
22
+ defaultAuthUserOnDelete,
19
23
  defaultFieldLabel,
20
24
  definePolicy,
21
25
  defineResource,
26
+ describeCondition,
27
+ enumMemberCollision,
28
+ enumTranslations,
22
29
  field,
23
30
  fieldSchema,
24
31
  fileKeyPrefix,
25
32
  fileMaxBytes,
26
33
  flagEmoji,
34
+ formValuesToInput,
27
35
  formatPhone,
36
+ formatValue,
28
37
  fromMinorUnits,
29
38
  hasTooManyPlaces,
30
39
  humanize,
40
+ initialFormValues,
41
+ isAuthUserTarget,
31
42
  isColor,
32
43
  isCountryCode,
33
44
  isDomain,
34
45
  isPhoneNumber,
35
46
  isSlug,
47
+ issuesByField,
36
48
  kebabCase,
37
49
  mimeTypesFor,
38
50
  moneyExponent,
39
51
  normalizeDomain,
52
+ optionLabel,
40
53
  ownershipFilter,
54
+ parseMultiValue,
41
55
  pascalCase,
42
56
  pluralize,
57
+ prismaEnumMember,
58
+ relationGraph,
43
59
  slugify,
44
60
  snakeCase,
45
61
  splitPhone,
62
+ statusTone,
46
63
  storedFields,
47
64
  toE164,
48
65
  toMinorUnits,
49
66
  words
50
- } from "./chunk-TB53VHXY.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";
51
82
 
52
83
  // src/password.ts
53
84
  var ALGORITHM = "pbkdf2-sha256";
@@ -367,8 +398,8 @@ function replay(chunks, reader, length) {
367
398
  });
368
399
  return fixed.readable;
369
400
  }
370
- function error(status, message) {
371
- return Response.json({ error: message }, { status });
401
+ function error(status, message, code) {
402
+ return problemResponse(status, message, code);
372
403
  }
373
404
  function createStorage(options) {
374
405
  const routePath = options.routePath ?? "/api/storage";
@@ -379,36 +410,36 @@ function createStorage(options) {
379
410
  const urlFor = (token) => `${options.baseURL?.replace(/\/+$/, "") ?? ""}${routePath}?token=${token}`;
380
411
  async function handle(request) {
381
412
  const token = new URL(request.url).searchParams.get("token");
382
- if (!token) return error(400, "Missing token.");
413
+ if (!token) return error(400, "Missing token.", PROBLEM_CODES.invalidToken);
383
414
  const payload = await verifyToken(await signingKey(), token);
384
415
  if (!payload || typeof payload.exp !== "number" || !isValidKey(payload.key)) {
385
- return error(403, "Invalid token.");
416
+ return error(403, "Invalid token.", PROBLEM_CODES.invalidToken);
386
417
  }
387
- 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);
388
419
  if (request.method === "PUT") {
389
- 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);
390
421
  const contentType = request.headers.get("content-type") ?? "";
391
422
  if (!matchesContentType(contentType, payload.types)) {
392
- 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);
393
424
  }
394
425
  const length = Number(request.headers.get("content-length"));
395
426
  if (!request.headers.has("content-length") || !Number.isInteger(length) || length < 0) {
396
- return error(411, "Content-Length is required.");
427
+ return error(411, "Content-Length is required.", PROBLEM_CODES.lengthRequired);
397
428
  }
398
- if (length > payload.max) return error(413, `File exceeds the ${payload.max}-byte limit.`);
399
- 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);
400
431
  const { head, chunks, reader } = await readHead(request.body, SNIFF_BYTES);
401
432
  if (sniffMatches(contentType, head) === false) {
402
433
  await drain(reader);
403
- 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);
404
435
  }
405
436
  await options.bucket.put(payload.key, replay(chunks, reader, length), { httpMetadata: { contentType } });
406
437
  return Response.json({ key: payload.key }, { status: 201 });
407
438
  }
408
439
  if (request.method === "GET" || request.method === "HEAD") {
409
- 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);
410
441
  const object = await options.bucket.get(payload.key);
411
- if (!object) return error(404, "Not found.");
442
+ if (!object) return error(404, "Not found.", PROBLEM_CODES.notFound);
412
443
  const headers = new Headers({
413
444
  "content-type": object.httpMetadata?.contentType ?? "application/octet-stream",
414
445
  "content-length": String(object.size),
@@ -445,9 +476,19 @@ function createStorage(options) {
445
476
  });
446
477
  return { url: urlFor(token), method: "PUT", key: upload.key, expiresAt: new Date(exp * 1e3) };
447
478
  },
448
- /** 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
+ */
449
486
  async createReadUrl(read) {
450
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
+ }
451
492
  const payload = { op: "get", key: read.key, exp: expiry(read.expiresIn) };
452
493
  if (read.downloadAs) payload.name = read.downloadAs;
453
494
  return urlFor(await signToken(await signingKey(), payload));
@@ -508,207 +549,6 @@ function randomLetters(length = 8) {
508
549
  return Array.from(bytes, (byte) => String.fromCharCode(97 + byte % 26)).join("");
509
550
  }
510
551
 
511
- // src/resource/relations.ts
512
- var RelationError = class extends Error {
513
- constructor(message) {
514
- super(message);
515
- this.name = "RelationError";
516
- }
517
- };
518
- var relationNameFor = (table, key) => `${table}_${columnName(key)}`;
519
- function relationGraph(resources) {
520
- const byName = new Map(resources.map((resource) => [resource.name, resource]));
521
- const graph = { byResource: {}, pending: [] };
522
- for (const resource of resources) {
523
- const relations = { belongsTo: [], hasMany: [] };
524
- for (const [key, def] of storedFields(resource)) {
525
- if (def.kind !== "belongsTo") continue;
526
- if (!byName.has(def.target)) {
527
- throw new RelationError(`${resource.name}.${key} belongs to "${def.target}", but there is no ${def.target} resource.`);
528
- }
529
- relations.belongsTo.push({
530
- key,
531
- name: key.replace(/Id$/, ""),
532
- target: def.target,
533
- required: def.required,
534
- onDelete: def.onDelete ?? "restrict",
535
- relationName: relationNameFor(resource.table, key)
536
- });
537
- }
538
- graph.byResource[resource.name] = relations;
539
- }
540
- for (const resource of resources) {
541
- for (const [key, def] of Object.entries(resource.fields)) {
542
- if (def.kind !== "hasMany") continue;
543
- const target = byName.get(def.target);
544
- if (!target) {
545
- graph.pending.push({ resource: resource.name, key, target: def.target });
546
- continue;
547
- }
548
- const foreignKey = def.foreignKey ?? `${camelCase(resource.name)}Id`;
549
- const back = graph.byResource[target.name].belongsTo.find((relation) => relation.key === foreignKey);
550
- if (!back || back.target !== resource.name) {
551
- throw new RelationError(
552
- `${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.`
553
- );
554
- }
555
- graph.byResource[resource.name].hasMany.push({ key, target: target.name, foreignKey, relationName: back.relationName });
556
- }
557
- }
558
- for (const resource of resources) {
559
- const { belongsTo, hasMany } = graph.byResource[resource.name];
560
- const taken = new Set(Object.keys(resource.fields));
561
- for (const { key, name } of belongsTo) {
562
- if (taken.has(name)) {
563
- throw new RelationError(`${resource.name}.${key} exposes relation "${name}", which collides with the ${resource.name}.${name} field. Rename one of them.`);
564
- }
565
- taken.add(name);
566
- }
567
- for (const { key } of hasMany) {
568
- if (belongsTo.some((relation) => relation.name === key)) {
569
- throw new RelationError(`${resource.name}.${key} collides with a belongsTo relation of the same name.`);
570
- }
571
- }
572
- }
573
- return graph;
574
- }
575
-
576
- // src/resource/display.ts
577
- var TONES = [
578
- ["success", /^(paid|fulfilled|active|approved|completed?|delivered|done|published|resolved|shipped|succeeded|success|verified|won)$/],
579
- ["danger", /^(banned|blocked|cancel+ed|churned|declined|error|expired|failed|lost|overdue|refunded|rejected|suspended)$/],
580
- ["warning", /^(awaiting|draft|in[-_ ]?review|on[-_ ]?hold|pending|processing|review|trial|unpaid|unverified)$/]
581
- ];
582
- function statusTone(value) {
583
- if (typeof value !== "string") return "neutral";
584
- const normalized = value.trim().toLowerCase();
585
- return TONES.find(([, pattern]) => pattern.test(normalized))?.[0] ?? "neutral";
586
- }
587
- function optionLabel(def, value) {
588
- if ((def.kind === "enum" || def.kind === "multiselect") && def.optionLabels?.[value]) return def.optionLabels[value];
589
- const text2 = value.replace(/[_-]+/g, " ").trim();
590
- return text2 ? text2[0].toUpperCase() + text2.slice(1) : value;
591
- }
592
- function formatValue(def, value, options = {}) {
593
- if (value === null || value === void 0 || value === "") return "\u2014";
594
- const locale = options.locale ?? "en-US";
595
- switch (def.kind) {
596
- case "boolean":
597
- return value ? "Yes" : "No";
598
- case "int":
599
- return new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }).format(Number(value));
600
- case "float":
601
- return new Intl.NumberFormat(locale, { maximumFractionDigits: 2 }).format(Number(value));
602
- case "date": {
603
- const date = /* @__PURE__ */ new Date(`${String(value)}T00:00:00Z`);
604
- return Number.isNaN(date.getTime()) ? String(value) : new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone: "UTC" }).format(date);
605
- }
606
- case "datetime":
607
- case "timestamp": {
608
- const date = value instanceof Date ? value : new Date(value);
609
- return Number.isNaN(date.getTime()) ? String(value) : new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short", timeZone: options.timeZone ?? "UTC" }).format(date);
610
- }
611
- case "enum":
612
- return optionLabel(def, String(value));
613
- case "multiselect":
614
- return Array.isArray(value) && value.length ? value.map((item) => optionLabel(def, String(item))).join(", ") : "\u2014";
615
- case "tags":
616
- return Array.isArray(value) && value.length ? value.map(String).join(", ") : "\u2014";
617
- case "json": {
618
- if (value === null || value === void 0) return "\u2014";
619
- const text2 = typeof value === "string" ? value : JSON.stringify(value);
620
- return text2.length > 60 ? `${text2.slice(0, 59)}\u2026` : text2;
621
- }
622
- case "string":
623
- if (def.format === "tel") return formatPhone(String(value));
624
- if (def.format === "country") return `${flagEmoji(String(value))} ${countryName(String(value), locale.split("-")[0])}`.trim();
625
- return String(value);
626
- case "file":
627
- return String(value).split("/").pop().replace(/^[0-9a-f-]{36}-/, "");
628
- default:
629
- return String(value);
630
- }
631
- }
632
-
633
- // src/resource/form.ts
634
- function initialFormValues(resource, record) {
635
- const values = {};
636
- for (const [key, def] of storedFields(resource)) {
637
- const source = record ? record[key] : "default" in def ? def.default : void 0;
638
- if (def.kind === "boolean") values[key] = source === true || source === 1;
639
- else if (def.kind === "multiselect" || def.kind === "tags") values[key] = JSON.stringify(Array.isArray(source) ? source : []);
640
- else if (def.kind === "json") values[key] = source === null || source === void 0 ? "" : JSON.stringify(source, null, 2);
641
- else if (source === null || source === void 0) values[key] = "";
642
- else if (source instanceof Date) values[key] = source.toISOString();
643
- else values[key] = String(source);
644
- }
645
- return values;
646
- }
647
- function formValuesToInput(resource, values, mode) {
648
- const input = {};
649
- for (const [key, def] of storedFields(resource)) {
650
- const raw = values[key];
651
- if (def.kind === "boolean") {
652
- input[key] = raw === true;
653
- continue;
654
- }
655
- if (def.kind === "multiselect" || def.kind === "tags") {
656
- const picked = parseMultiValue(raw);
657
- if (picked.length === 0 && mode === "create" && "default" in def && def.default !== void 0) continue;
658
- input[key] = picked.length === 0 && !def.required ? null : picked;
659
- continue;
660
- }
661
- if (def.kind === "json") {
662
- const source = typeof raw === "string" ? raw.trim() : "";
663
- if (source === "") {
664
- if (mode === "create" && "default" in def && def.default !== void 0) continue;
665
- if (!def.required) input[key] = null;
666
- continue;
667
- }
668
- try {
669
- input[key] = JSON.parse(source);
670
- } catch {
671
- input[key] = Number.NaN;
672
- }
673
- continue;
674
- }
675
- const text2 = typeof raw === "string" ? raw : "";
676
- const empty = text2.trim() === "";
677
- if (empty) {
678
- const hasDefault = "default" in def && def.default !== void 0;
679
- if (mode === "create" && hasDefault) continue;
680
- if (!def.required) input[key] = null;
681
- else if (def.kind === "string" || def.kind === "text") input[key] = text2;
682
- continue;
683
- }
684
- if (def.kind === "int" || def.kind === "float") {
685
- const number = Number(text2.trim());
686
- input[key] = Number.isFinite(number) ? number : text2;
687
- continue;
688
- }
689
- input[key] = text2;
690
- }
691
- return input;
692
- }
693
- function parseMultiValue(raw) {
694
- if (typeof raw !== "string" || raw === "") return [];
695
- try {
696
- const parsed = JSON.parse(raw);
697
- return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
698
- } catch {
699
- return [];
700
- }
701
- }
702
- function issuesByField(issues) {
703
- const errors = {};
704
- for (const issue of issues) {
705
- const path = Array.isArray(issue.path) ? issue.path.map(String).join(".") : issue.path;
706
- const key = path.split(".")[0] || "_form";
707
- errors[key] ??= issue.message;
708
- }
709
- return errors;
710
- }
711
-
712
552
  // src/seed.ts
713
553
  async function credentialUserRows(input) {
714
554
  const email = input.email.trim().toLowerCase();
@@ -1052,16 +892,20 @@ function createFake(seed = 20260101) {
1052
892
  var fake = createFake();
1053
893
 
1054
894
  // src/index.ts
1055
- var FLARE_VERSION = "0.10.0";
895
+ var FLARE_VERSION = "0.12.0";
1056
896
  export {
897
+ AUTH_USER,
1057
898
  DEFAULT_FILE_MAX_BYTES,
1058
899
  DEFAULT_MONEY_EXPONENT,
1059
900
  FILE_CATEGORIES,
1060
901
  FLARE_VERSION,
902
+ ISSUE_CODES,
1061
903
  NUMBER_FORMATS,
1062
904
  PBKDF2_ITERATIONS,
905
+ PROBLEM_CODES,
1063
906
  PolicyError,
1064
907
  RESERVED_FIELD_NAMES,
908
+ RateSyntaxError,
1065
909
  RelationError,
1066
910
  ResourceDefinitionError,
1067
911
  SNIFF_BYTES,
@@ -1074,6 +918,7 @@ export {
1074
918
  checkoutPaid,
1075
919
  clientResource,
1076
920
  columnName,
921
+ conditionHolds,
1077
922
  countries,
1078
923
  countryName,
1079
924
  createFake,
@@ -1083,15 +928,20 @@ export {
1083
928
  createValidators,
1084
929
  credentialUserRows,
1085
930
  currentPeriodEnd,
931
+ defaultAuthUserOnDelete,
1086
932
  defaultFieldLabel,
1087
933
  definePolicy,
1088
934
  defineResource,
1089
935
  defineSeed,
936
+ describeCondition,
937
+ enumMemberCollision,
938
+ enumTranslations,
1090
939
  fake,
1091
940
  field,
1092
941
  fieldSchema,
1093
942
  fileKeyPrefix,
1094
943
  fileMaxBytes,
944
+ fixedWindow,
1095
945
  flagEmoji,
1096
946
  formValuesToInput,
1097
947
  formatPhone,
@@ -1103,6 +953,7 @@ export {
1103
953
  humanize,
1104
954
  importSigningKey,
1105
955
  initialFormValues,
956
+ isAuthUserTarget,
1106
957
  isColor,
1107
958
  isCountryCode,
1108
959
  isDomain,
@@ -1118,9 +969,15 @@ export {
1118
969
  optionLabel,
1119
970
  ownershipFilter,
1120
971
  parseMultiValue,
972
+ parseRate,
1121
973
  pascalCase,
1122
974
  pluralize,
975
+ prismaEnumMember,
976
+ problemResponse,
1123
977
  randomLetters,
978
+ rateFor,
979
+ rateLimitKey,
980
+ ratesIn,
1124
981
  relationGraph,
1125
982
  renderTransactionalEmail,
1126
983
  shouldApplySubscription,
@@ -1138,5 +995,6 @@ export {
1138
995
  toSubscriptionStatus,
1139
996
  verifyPassword,
1140
997
  verifyToken,
998
+ wantsJson,
1141
999
  words
1142
1000
  };