@flaredev/core 0.10.0 → 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.
@@ -266,6 +266,26 @@ function storedFields(resource) {
266
266
  }
267
267
  var columnName = (key) => snakeCase(key);
268
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
+
269
289
  // src/resource/formats.ts
270
290
  import {
271
291
  getCountries,
@@ -480,15 +500,30 @@ function createValidators(resource) {
480
500
  "This file wasn't uploaded for this field"
481
501
  );
482
502
  }
483
- const value = def.required ? base : base.nullable();
503
+ const conditional2 = def.showWhen !== void 0;
504
+ const value = def.required && !conditional2 ? base : base.nullable();
484
505
  update[key] = value.optional();
485
506
  const hasDefault = "default" in def && def.default !== void 0;
486
- create[key] = def.required && !hasDefault ? value : value.optional();
507
+ create[key] = def.required && !conditional2 && !hasDefault ? value : value.optional();
487
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
+ };
488
523
  const unknownKeys = { error: (issue) => issue.code === "unrecognized_keys" ? `Unknown field(s): ${issue.keys?.join(", ")}` : void 0 };
489
524
  return {
490
- create: z.strictObject(create, unknownKeys),
491
- update: z.strictObject(update, unknownKeys)
525
+ create: z.strictObject(create, unknownKeys).superRefine(checkConditions(false)),
526
+ update: z.strictObject(update, unknownKeys).superRefine(checkConditions(true))
492
527
  };
493
528
  }
494
529
 
@@ -581,6 +616,8 @@ export {
581
616
  defineResource,
582
617
  storedFields,
583
618
  columnName,
619
+ conditionHolds,
620
+ describeCondition,
584
621
  flagEmoji,
585
622
  countryName,
586
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
  /**
@@ -626,6 +626,6 @@ type SeedUser = (user: SeedUserInput) => Promise<CredentialUserRows["user"]>;
626
626
  type Seed = (context: SeedContext) => void | Promise<void>;
627
627
  declare function defineSeed(seed: Seed): Seed;
628
628
 
629
- declare const FLARE_VERSION = "0.10.0";
629
+ declare const FLARE_VERSION = "0.11.0";
630
630
 
631
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
@@ -13,12 +13,14 @@ import {
13
13
  can,
14
14
  clientResource,
15
15
  columnName,
16
+ conditionHolds,
16
17
  countries,
17
18
  countryName,
18
19
  createValidators,
19
20
  defaultFieldLabel,
20
21
  definePolicy,
21
22
  defineResource,
23
+ describeCondition,
22
24
  field,
23
25
  fieldSchema,
24
26
  fileKeyPrefix,
@@ -47,7 +49,7 @@ import {
47
49
  toE164,
48
50
  toMinorUnits,
49
51
  words
50
- } from "./chunk-TB53VHXY.js";
52
+ } from "./chunk-62JI5DCR.js";
51
53
 
52
54
  // src/password.ts
53
55
  var ALGORITHM = "pbkdf2-sha256";
@@ -1052,7 +1054,7 @@ function createFake(seed = 20260101) {
1052
1054
  var fake = createFake();
1053
1055
 
1054
1056
  // src/index.ts
1055
- var FLARE_VERSION = "0.10.0";
1057
+ var FLARE_VERSION = "0.11.0";
1056
1058
  export {
1057
1059
  DEFAULT_FILE_MAX_BYTES,
1058
1060
  DEFAULT_MONEY_EXPONENT,
@@ -1074,6 +1076,7 @@ export {
1074
1076
  checkoutPaid,
1075
1077
  clientResource,
1076
1078
  columnName,
1079
+ conditionHolds,
1077
1080
  countries,
1078
1081
  countryName,
1079
1082
  createFake,
@@ -1087,6 +1090,7 @@ export {
1087
1090
  definePolicy,
1088
1091
  defineResource,
1089
1092
  defineSeed,
1093
+ describeCondition,
1090
1094
  fake,
1091
1095
  field,
1092
1096
  fieldSchema,
@@ -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
@@ -4,7 +4,7 @@ import {
4
4
  createValidators,
5
5
  moneyExponent,
6
6
  storedFields
7
- } from "./chunk-TB53VHXY.js";
7
+ } from "./chunk-62JI5DCR.js";
8
8
 
9
9
  // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/entity.js
10
10
  var entityKind = /* @__PURE__ */ Symbol.for("drizzle:entityKind");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flaredev/core",
3
- "version": "0.10.0",
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",