@flaredev/core 0.9.1 → 0.11.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.
@@ -91,6 +91,60 @@ var field = {
91
91
  hasMany: (target, options) => ({ kind: "hasMany", target, ...options, required: false })
92
92
  };
93
93
 
94
+ // src/resource/money.ts
95
+ var EXPONENTS = {
96
+ // No minor unit at all.
97
+ BIF: 0,
98
+ CLP: 0,
99
+ DJF: 0,
100
+ GNF: 0,
101
+ ISK: 0,
102
+ JPY: 0,
103
+ KMF: 0,
104
+ KRW: 0,
105
+ PYG: 0,
106
+ RWF: 0,
107
+ UGX: 0,
108
+ UYI: 0,
109
+ VND: 0,
110
+ VUV: 0,
111
+ XAF: 0,
112
+ XOF: 0,
113
+ XPF: 0,
114
+ // Thousandths.
115
+ BHD: 3,
116
+ IQD: 3,
117
+ JOD: 3,
118
+ KWD: 3,
119
+ LYD: 3,
120
+ OMR: 3,
121
+ TND: 3,
122
+ // Ten-thousandths.
123
+ CLF: 4
124
+ };
125
+ var DEFAULT_MONEY_EXPONENT = 2;
126
+ function moneyExponent(currency) {
127
+ if (!currency) return DEFAULT_MONEY_EXPONENT;
128
+ return EXPONENTS[currency.toUpperCase()] ?? DEFAULT_MONEY_EXPONENT;
129
+ }
130
+ var factor = (exponent) => 10 ** exponent;
131
+ function toMinorUnits(amount, currency) {
132
+ if (!Number.isFinite(amount)) throw new TypeError(`Not an amount: ${amount}`);
133
+ return Number(amount.toFixed(moneyExponent(currency)).replace(".", ""));
134
+ }
135
+ function fromMinorUnits(minor, currency) {
136
+ const exponent = moneyExponent(currency);
137
+ if (exponent === 0) return minor;
138
+ return Number((minor / factor(exponent)).toFixed(exponent));
139
+ }
140
+ function hasTooManyPlaces(amount, currency) {
141
+ const exponent = moneyExponent(currency);
142
+ const text = String(amount);
143
+ if (text.includes("e") || text.includes("E")) return true;
144
+ const places = text.includes(".") ? text.length - text.indexOf(".") - 1 : 0;
145
+ return places > exponent;
146
+ }
147
+
94
148
  // src/resource/naming.ts
95
149
  function words(value) {
96
150
  return value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map((word) => word.toLowerCase());
@@ -212,6 +266,26 @@ function storedFields(resource) {
212
266
  }
213
267
  var columnName = (key) => snakeCase(key);
214
268
 
269
+ // src/resource/conditions.ts
270
+ function conditionHolds(condition, values) {
271
+ if (!condition) return true;
272
+ const actual = values[condition.field];
273
+ const text = actual === null || actual === void 0 ? "" : String(actual);
274
+ switch (condition.op) {
275
+ case "eq":
276
+ return text === String(condition.value);
277
+ case "ne":
278
+ return text !== String(condition.value);
279
+ case "in":
280
+ return (Array.isArray(condition.value) ? condition.value : [condition.value]).some((option) => String(option) === text);
281
+ }
282
+ }
283
+ function describeCondition(condition) {
284
+ const values = Array.isArray(condition.value) ? condition.value : [condition.value];
285
+ if (condition.op === "in") return `${condition.field} is one of ${values.join(", ")}`;
286
+ return `${condition.field} ${condition.op === "eq" ? "is" : "is not"} ${values[0]}`;
287
+ }
288
+
215
289
  // src/resource/formats.ts
216
290
  import {
217
291
  getCountries,
@@ -367,7 +441,14 @@ function fieldSchema(def) {
367
441
  if (def.format === "rating") {
368
442
  s = s.min(def.min ?? 0, `At least ${def.min ?? 0}`).max(def.max ?? 5, `At most ${def.max ?? 5}`);
369
443
  }
370
- if (def.format === "money" && def.min === void 0) s = s.min(0, "Cannot be negative");
444
+ if (def.format === "money") {
445
+ if (def.min === void 0) s = s.min(0, "Cannot be negative");
446
+ const places = moneyExponent(def.currency);
447
+ s = s.refine(
448
+ (value) => !hasTooManyPlaces(value, def.currency),
449
+ places === 0 ? "A whole amount, with no decimal places" : `At most ${places} decimal place${places === 1 ? "" : "s"}`
450
+ );
451
+ }
371
452
  if (def.min !== void 0) s = s.min(def.min, `Must be at least ${def.min}`);
372
453
  if (def.max !== void 0) s = s.max(def.max, `Must be at most ${def.max}`);
373
454
  return s;
@@ -419,15 +500,30 @@ function createValidators(resource) {
419
500
  "This file wasn't uploaded for this field"
420
501
  );
421
502
  }
422
- const value = def.required ? base : base.nullable();
503
+ const conditional2 = def.showWhen !== void 0;
504
+ const value = def.required && !conditional2 ? base : base.nullable();
423
505
  update[key] = value.optional();
424
506
  const hasDefault = "default" in def && def.default !== void 0;
425
- create[key] = def.required && !hasDefault ? value : value.optional();
507
+ create[key] = def.required && !conditional2 && !hasDefault ? value : value.optional();
426
508
  }
509
+ const conditional = [...storedFields(resource)].flatMap(
510
+ ([key, def]) => def.showWhen ? [{ key, condition: def.showWhen, required: def.required !== false }] : []
511
+ );
512
+ const checkConditions = (partial) => (values, ctx) => {
513
+ for (const { key, condition, required } of conditional) {
514
+ if (!required) continue;
515
+ if (partial && !(condition.field in values) && !(key in values)) continue;
516
+ if (!conditionHolds(condition, values)) continue;
517
+ const supplied = values[key];
518
+ if (supplied === void 0 || supplied === null || supplied === "") {
519
+ ctx.addIssue({ code: "custom", path: [key], message: `Required when ${describeCondition(condition)}` });
520
+ }
521
+ }
522
+ };
427
523
  const unknownKeys = { error: (issue) => issue.code === "unrecognized_keys" ? `Unknown field(s): ${issue.keys?.join(", ")}` : void 0 };
428
524
  return {
429
- create: z.strictObject(create, unknownKeys),
430
- update: z.strictObject(update, unknownKeys)
525
+ create: z.strictObject(create, unknownKeys).superRefine(checkConditions(false)),
526
+ update: z.strictObject(update, unknownKeys).superRefine(checkConditions(true))
431
527
  };
432
528
  }
433
529
 
@@ -501,6 +597,11 @@ export {
501
597
  DEFAULT_FILE_MAX_BYTES,
502
598
  fileMaxBytes,
503
599
  field,
600
+ DEFAULT_MONEY_EXPONENT,
601
+ moneyExponent,
602
+ toMinorUnits,
603
+ fromMinorUnits,
604
+ hasTooManyPlaces,
504
605
  words,
505
606
  pascalCase,
506
607
  camelCase,
@@ -515,6 +616,8 @@ export {
515
616
  defineResource,
516
617
  storedFields,
517
618
  columnName,
619
+ conditionHolds,
620
+ describeCondition,
518
621
  flagEmoji,
519
622
  countryName,
520
623
  countries,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
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
+ 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';
3
3
  import { z } from 'zod';
4
4
 
5
5
  /**
@@ -285,6 +285,52 @@ declare const checkoutPaid: (session: {
285
285
  /** `8 random lowercase letters`, the suffix Stripe asks for on `integration_identifier`. */
286
286
  declare function randomLetters(length?: number): string;
287
287
 
288
+ /**
289
+ * Money, stored exactly.
290
+ *
291
+ * A `money` field used to be a `float`, and `0.1 + 0.2` is the reason that was a bug
292
+ * waiting for a shop with a till in it. Binary floating point cannot represent most
293
+ * decimal fractions, so a column of them drifts: sum a few thousand prices and the total
294
+ * is wrong by a cent, which is the kind of wrong nobody can explain to an accountant.
295
+ *
296
+ * So the column holds **minor units** — whole cents, whole fils, whole yen — and an
297
+ * integer column is exact. The store converts at the boundary, so everything above it
298
+ * still works in the units people write and read: the API sends 19.99, the form shows
299
+ * 19.99, and only the database sees 1999.
300
+ *
301
+ * Flare's own billing code has always done this (`amount:int`, formatted as `cents / 100`).
302
+ * This brings `money` fields in line with it.
303
+ */
304
+ /** The default when a field names no currency: two places, which is almost everywhere. */
305
+ declare const DEFAULT_MONEY_EXPONENT = 2;
306
+ /** Decimal places for an ISO 4217 code. Unknown codes get two, which is the common case. */
307
+ declare function moneyExponent(currency?: string): number;
308
+ /**
309
+ * A decimal amount as whole minor units: 19.99 → 1999.
310
+ *
311
+ * Through `toFixed` rather than `Math.round(amount * 100)`, because multiplying first adds
312
+ * an error of its own: `2.675 * 100` is 267.50000000000006, which rounds to 268, while the
313
+ * double behind `2.675` is 2.67499999999999982 and rounds to 267. `toFixed` asks the
314
+ * runtime to round the value that is actually there, so this agrees with what every other
315
+ * tool prints for the same number.
316
+ *
317
+ * Neither approach can rescue a value the double never held — `1.005` is 1.00499999999999989
318
+ * and becomes 100, not 101. Nothing short of decimal arithmetic all the way from the client
319
+ * fixes that, which is why the validator refuses an amount with more places than its
320
+ * currency has: the place to catch it is before it is a double at all.
321
+ */
322
+ declare function toMinorUnits(amount: number, currency?: string): number;
323
+ /** Whole minor units as a decimal amount: 1999 → 19.99. */
324
+ declare function fromMinorUnits(minor: number, currency?: string): number;
325
+ /**
326
+ * Whether an amount has more decimal places than its currency has.
327
+ *
328
+ * 19.999 in a two-place currency is not a rounding problem to solve quietly — it is a
329
+ * caller sending something they did not mean, and storing 20.00 instead would be a silent
330
+ * change to a number that matters.
331
+ */
332
+ declare function hasTooManyPlaces(amount: number, currency?: string): boolean;
333
+
288
334
  /**
289
335
  * Zod schemas derived from a descriptor at runtime. Unknown keys are rejected,
290
336
  * so clients can't set `id`, timestamps or undeclared columns (no mass assignment).
@@ -525,10 +571,61 @@ interface SeedContext {
525
571
  insertMany: InsertMany;
526
572
  /** Sample values: names, emails, sentences, dates. */
527
573
  fake: Fake;
574
+ /**
575
+ * Creates a user with a credential account, so you can sign in as them straight away:
576
+ *
577
+ * await seedUser({ email: "ada@example.com", password: "correct-horse-battery", role: "admin" });
578
+ */
579
+ seedUser: SeedUser;
580
+ }
581
+ /** What `seedUser` needs to make somebody who can sign in. */
582
+ interface SeedUserInput {
583
+ email: string;
584
+ /** Stored as the same PBKDF2 hash the sign-up route writes, so sign-in verifies it. */
585
+ password: string;
586
+ name?: string;
587
+ /** Seeded users are verified by default: an unverified one cannot sign in. */
588
+ emailVerified?: boolean;
589
+ /** e.g. "admin", which the dashboard's policies read. */
590
+ role?: string;
528
591
  }
592
+ /** A user row and the credential account row that lets them sign in. */
593
+ interface CredentialUserRows {
594
+ user: {
595
+ id: string;
596
+ name: string;
597
+ email: string;
598
+ emailVerified: boolean;
599
+ role: string | undefined;
600
+ createdAt: Date;
601
+ updatedAt: Date;
602
+ };
603
+ account: {
604
+ id: string;
605
+ accountId: string;
606
+ providerId: string;
607
+ userId: string;
608
+ password: string;
609
+ createdAt: Date;
610
+ updatedAt: Date;
611
+ };
612
+ }
613
+ /**
614
+ * The two rows Better Auth looks for when somebody signs in with a password.
615
+ *
616
+ * `accountId` is the one that is easy to get wrong. It is not the provider's name and it is
617
+ * not the email: for a credential account Better Auth writes the user's own id into that
618
+ * column at sign-up, and looks the account up by it at sign-in. Put the email there instead
619
+ * and everything looks right — the row exists, the hash is correct — but signing in answers
620
+ * "User not found", which is a long afternoon. Both rows are built here so that rule lives
621
+ * in one place rather than in every seed somebody writes by hand.
622
+ */
623
+ declare function credentialUserRows(input: SeedUserInput): Promise<CredentialUserRows>;
624
+ /** Creates a user who can actually sign in, and returns the row. */
625
+ type SeedUser = (user: SeedUserInput) => Promise<CredentialUserRows["user"]>;
529
626
  type Seed = (context: SeedContext) => void | Promise<void>;
530
627
  declare function defineSeed(seed: Seed): Seed;
531
628
 
532
- declare const FLARE_VERSION = "0.9.1";
629
+ declare const FLARE_VERSION = "0.11.0";
533
630
 
534
- export { type BelongsToRelation, type Country, CreateInput, 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 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, currentPeriodEnd, defineSeed, fake, fieldSchema, fileKeyPrefix, flagEmoji, formValuesToInput, formatPhone, formatValue, grantsAccess, hashPassword, humanize, importSigningKey, initialFormValues, isColor, isCountryCode, isDomain, isLiveSubscription, isPhoneNumber, isSlug, issuesByField, kebabCase, matchesContentType, normalizeDomain, optionLabel, parseMultiValue, pascalCase, pluralize, randomLetters, relationGraph, renderTransactionalEmail, shouldApplySubscription, signToken, slugify, snakeCase, sniffMatches, splitPhone, statusTone, subscriptionPriceId, subscriptionState, toE164, toSubscriptionStatus, verifyPassword, verifyToken, words };
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 };
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  DEFAULT_FILE_MAX_BYTES,
3
+ DEFAULT_MONEY_EXPONENT,
3
4
  FILE_CATEGORIES,
4
5
  NUMBER_FORMATS,
5
6
  PolicyError,
@@ -12,18 +13,22 @@ import {
12
13
  can,
13
14
  clientResource,
14
15
  columnName,
16
+ conditionHolds,
15
17
  countries,
16
18
  countryName,
17
19
  createValidators,
18
20
  defaultFieldLabel,
19
21
  definePolicy,
20
22
  defineResource,
23
+ describeCondition,
21
24
  field,
22
25
  fieldSchema,
23
26
  fileKeyPrefix,
24
27
  fileMaxBytes,
25
28
  flagEmoji,
26
29
  formatPhone,
30
+ fromMinorUnits,
31
+ hasTooManyPlaces,
27
32
  humanize,
28
33
  isColor,
29
34
  isCountryCode,
@@ -32,6 +37,7 @@ import {
32
37
  isSlug,
33
38
  kebabCase,
34
39
  mimeTypesFor,
40
+ moneyExponent,
35
41
  normalizeDomain,
36
42
  ownershipFilter,
37
43
  pascalCase,
@@ -41,8 +47,9 @@ import {
41
47
  splitPhone,
42
48
  storedFields,
43
49
  toE164,
50
+ toMinorUnits,
44
51
  words
45
- } from "./chunk-AAXNSIZG.js";
52
+ } from "./chunk-62JI5DCR.js";
46
53
 
47
54
  // src/password.ts
48
55
  var ALGORITHM = "pbkdf2-sha256";
@@ -705,6 +712,34 @@ function issuesByField(issues) {
705
712
  }
706
713
 
707
714
  // src/seed.ts
715
+ async function credentialUserRows(input) {
716
+ const email = input.email.trim().toLowerCase();
717
+ if (!email.includes("@")) throw new Error(`seedUser needs an email address, got "${input.email}".`);
718
+ if (!input.password) throw new Error(`seedUser needs a password for ${email}.`);
719
+ const id = crypto.randomUUID();
720
+ const now = /* @__PURE__ */ new Date();
721
+ return {
722
+ user: {
723
+ id,
724
+ name: input.name ?? email.split("@")[0],
725
+ email,
726
+ emailVerified: input.emailVerified ?? true,
727
+ role: input.role,
728
+ createdAt: now,
729
+ updatedAt: now
730
+ },
731
+ account: {
732
+ id: crypto.randomUUID(),
733
+ // Not the email. See above.
734
+ accountId: id,
735
+ providerId: "credential",
736
+ userId: id,
737
+ password: await hashPassword(input.password),
738
+ createdAt: now,
739
+ updatedAt: now
740
+ }
741
+ };
742
+ }
708
743
  function defineSeed(seed) {
709
744
  return seed;
710
745
  }
@@ -1019,9 +1054,10 @@ function createFake(seed = 20260101) {
1019
1054
  var fake = createFake();
1020
1055
 
1021
1056
  // src/index.ts
1022
- var FLARE_VERSION = "0.9.1";
1057
+ var FLARE_VERSION = "0.11.0";
1023
1058
  export {
1024
1059
  DEFAULT_FILE_MAX_BYTES,
1060
+ DEFAULT_MONEY_EXPONENT,
1025
1061
  FILE_CATEGORIES,
1026
1062
  FLARE_VERSION,
1027
1063
  NUMBER_FORMATS,
@@ -1040,6 +1076,7 @@ export {
1040
1076
  checkoutPaid,
1041
1077
  clientResource,
1042
1078
  columnName,
1079
+ conditionHolds,
1043
1080
  countries,
1044
1081
  countryName,
1045
1082
  createFake,
@@ -1047,11 +1084,13 @@ export {
1047
1084
  createObjectKey,
1048
1085
  createStorage,
1049
1086
  createValidators,
1087
+ credentialUserRows,
1050
1088
  currentPeriodEnd,
1051
1089
  defaultFieldLabel,
1052
1090
  definePolicy,
1053
1091
  defineResource,
1054
1092
  defineSeed,
1093
+ describeCondition,
1055
1094
  fake,
1056
1095
  field,
1057
1096
  fieldSchema,
@@ -1061,7 +1100,9 @@ export {
1061
1100
  formValuesToInput,
1062
1101
  formatPhone,
1063
1102
  formatValue,
1103
+ fromMinorUnits,
1064
1104
  grantsAccess,
1105
+ hasTooManyPlaces,
1065
1106
  hashPassword,
1066
1107
  humanize,
1067
1108
  importSigningKey,
@@ -1076,6 +1117,7 @@ export {
1076
1117
  kebabCase,
1077
1118
  matchesContentType,
1078
1119
  mimeTypesFor,
1120
+ moneyExponent,
1079
1121
  normalizeDomain,
1080
1122
  optionLabel,
1081
1123
  ownershipFilter,
@@ -1096,6 +1138,7 @@ export {
1096
1138
  subscriptionPriceId,
1097
1139
  subscriptionState,
1098
1140
  toE164,
1141
+ toMinorUnits,
1099
1142
  toSubscriptionStatus,
1100
1143
  verifyPassword,
1101
1144
  verifyToken,
@@ -1,3 +1,43 @@
1
+ /**
2
+ * Fields that only apply sometimes.
3
+ *
4
+ * A billing cycle is required on a recurring project and meaningless on a one-off one.
5
+ * The usual way that gets built is an effect in the admin form that hides the input, and
6
+ * then the API happily accepts a cycle on a one-off project because nothing told it not
7
+ * to. The rule ends up in two or three places that disagree the first time somebody
8
+ * changes it.
9
+ *
10
+ * So the condition is declared on the field instead, once:
11
+ *
12
+ * billingCycle: field.enum(["monthly", "annually"], {
13
+ * showWhen: { field: "kind", op: "eq", value: "recurring" },
14
+ * })
15
+ *
16
+ * and everything that needs it reads the same object: the validator makes it required
17
+ * only when the condition holds, the store clears it when it doesn't, and the form shows
18
+ * the input only while it applies.
19
+ */
20
+ interface FieldCondition {
21
+ /** The field this one depends on. Must be a field of the same resource. */
22
+ field: string;
23
+ op: "eq" | "ne" | "in";
24
+ /** A single value for eq/ne, a list for `in`. */
25
+ value: string | string[];
26
+ }
27
+ /**
28
+ * Whether a conditional field applies, given the record it sits in.
29
+ *
30
+ * Compared as text, because the controlling field is an enum, a boolean or a number
31
+ * depending on who wrote the descriptor, and a form sends every one of them as a string.
32
+ * `true` and `"true"` have to answer the same here or the form and the API disagree.
33
+ *
34
+ * A field with no condition always applies, which is what makes this safe to call on
35
+ * every field without asking first.
36
+ */
37
+ declare function conditionHolds(condition: FieldCondition | undefined, values: Record<string, unknown>): boolean;
38
+ /** "kind == recurring" — for an error message, a form hint or a generated comment. */
39
+ declare function describeCondition(condition: FieldCondition): string;
40
+
1
41
  /**
2
42
  * Field builders for resource descriptors.
3
43
  *
@@ -6,6 +46,7 @@
6
46
  * rule in a descriptor takes effect without re-running the generator. Only
7
47
  * storage-level changes (kind, nullability, uniqueness, relations) need a migration.
8
48
  */
49
+
9
50
  type FieldKind = "string" | "text" | "int" | "float" | "boolean" | "date" | "datetime" | "enum" | "multiselect" | "tags" | "json" | "file" | "belongsTo" | "hasMany";
10
51
  /** Options shared by every stored (non-hasMany) field. */
11
52
  interface CommonOptions<T> {
@@ -23,6 +64,14 @@ interface CommonOptions<T> {
23
64
  filterable?: boolean;
24
65
  /** Included in free-text search. Default true for string fields. */
25
66
  searchable?: boolean;
67
+ /**
68
+ * Only applies when another field says so, e.g. a billing cycle on a recurring project:
69
+ * `showWhen: { field: "kind", op: "eq", value: "recurring" }`.
70
+ *
71
+ * The column is always nullable, because the field is genuinely absent the rest of the
72
+ * time. `required` then means "required *when it applies*" rather than "never null".
73
+ */
74
+ showWhen?: FieldCondition;
26
75
  }
27
76
  /**
28
77
  * What a string holds. Each format is still a text column; the format decides
@@ -508,4 +557,4 @@ declare function ownershipFilter(policy: Policy | undefined | null, user: {
508
557
  /** Actions `role` may perform, for hiding UI it can't use. */
509
558
  declare function allowedActions(policy: Policy | undefined | null, role: string | null | undefined): Record<Exclude<PolicyAction, "list">, boolean>;
510
559
 
511
- export { storedFields as $, type TextFormat as A, type BelongsToField as B, type CreateInput as C, DEFAULT_FILE_MAX_BYTES as D, type EnumField as E, type Field as F, allowedActions as G, type HasManyField as H, type IntField as I, type JsonField as J, can as K, clientResource as L, type MultiSelectField as M, NUMBER_FORMATS as N, columnName as O, type Policy as P, defaultFieldLabel as Q, type Resource as R, type StoredField as S, TEXT_FORMATS as T, type UpdateInput as U, definePolicy as V, defineResource as W, field as X, fileMaxBytes as Y, mimeTypesFor as Z, ownershipFilter as _, type BooleanField as a, type ClientResource as b, type DateField as c, type DateTimeField as d, FILE_CATEGORIES as e, type FieldKind as f, type FieldValue as g, type FileCategory as h, type FileField as i, type FloatField as j, type JsonOptions as k, type NumberFormat as l, type PolicyAction as m, type PolicyConfig as n, PolicyError as o, type PolicyOwnership as p, RESERVED_FIELD_NAMES as q, type RecordOf as r, type ResourceConfig as s, ResourceDefinitionError as t, STRING_FORMATS as u, type StringField as v, type StringFormat as w, type TagsField as x, type TagsOptions as y, type TextField as z };
560
+ export { fileMaxBytes as $, type TextField as A, type BelongsToField as B, type CreateInput as C, DEFAULT_FILE_MAX_BYTES as D, type EnumField as E, type Field as F, type TextFormat as G, type HasManyField as H, type IntField as I, type JsonField as J, allowedActions as K, can as L, type MultiSelectField as M, NUMBER_FORMATS as N, clientResource as O, type Policy as P, columnName as Q, type Resource as R, type StoredField as S, TEXT_FORMATS as T, type UpdateInput as U, conditionHolds as V, defaultFieldLabel as W, definePolicy as X, defineResource as Y, describeCondition as Z, field as _, type BooleanField as a, mimeTypesFor as a0, ownershipFilter as a1, storedFields as a2, type ClientResource as b, type DateField as c, type DateTimeField as d, FILE_CATEGORIES as e, type FieldCondition as f, type FieldKind as g, type FieldValue as h, type FileCategory as i, type FileField as j, type FloatField as k, type JsonOptions as l, type NumberFormat as m, type PolicyAction as n, type PolicyConfig as o, PolicyError as p, type PolicyOwnership as q, RESERVED_FIELD_NAMES as r, type RecordOf as s, type ResourceConfig as t, ResourceDefinitionError as u, STRING_FORMATS as v, type StringField as w, type StringFormat as x, type TagsField as y, type TagsOptions as z };
package/dist/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
2
2
  import { BaseSQLiteDatabase, SQLiteTable, SQLiteColumn } from 'drizzle-orm/sqlite-core';
3
- import { S as StoredField, R as Resource, F as Field, P as Policy } from './policy-DFimkxdx.js';
3
+ import { S as StoredField, R as Resource, F as Field, P as Policy } from './policy-DaK3dWQ_.js';
4
4
 
5
5
  /**
6
6
  * The handful of things a resource store needs a database to do.
package/dist/server.js CHANGED
@@ -2,8 +2,9 @@ import {
2
2
  can,
3
3
  columnName,
4
4
  createValidators,
5
+ moneyExponent,
5
6
  storedFields
6
- } from "./chunk-AAXNSIZG.js";
7
+ } from "./chunk-62JI5DCR.js";
7
8
 
8
9
  // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/entity.js
9
10
  var entityKind = /* @__PURE__ */ Symbol.for("drizzle:entityKind");
@@ -1783,6 +1784,12 @@ function fieldJsonSchema(def) {
1783
1784
  const schema = { ...base, type: def.kind === "int" ? "integer" : "number" };
1784
1785
  if (def.min !== void 0) schema.minimum = def.min;
1785
1786
  if (def.max !== void 0) schema.maximum = def.max;
1787
+ if (def.format === "money") {
1788
+ const places = moneyExponent(def.currency);
1789
+ schema.multipleOf = places === 0 ? 1 : Number(`1e-${places}`);
1790
+ schema.description = base.description ?? `An amount in ${def.currency ?? "the app's currency"}, to ${places} decimal place${places === 1 ? "" : "s"}.`;
1791
+ schema.examples = [places === 0 ? 1500 : 19.99];
1792
+ }
1786
1793
  return schema;
1787
1794
  }
1788
1795
  case "boolean":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flaredev/core",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Flare runtime: resource descriptors, field grammar, and Cloudflare-native helpers",
5
5
  "license": "MIT",
6
6
  "author": "Muke Johnbaptist",