@flaredev/core 0.1.2 → 0.3.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.js CHANGED
@@ -1,21 +1,44 @@
1
1
  import {
2
+ DEFAULT_FILE_MAX_BYTES,
3
+ FILE_CATEGORIES,
4
+ PolicyError,
2
5
  RESERVED_FIELD_NAMES,
3
6
  ResourceDefinitionError,
7
+ STRING_FORMATS,
8
+ allowedActions,
4
9
  camelCase,
10
+ can,
5
11
  columnName,
12
+ countries,
13
+ countryName,
6
14
  createValidators,
7
15
  defaultFieldLabel,
16
+ definePolicy,
8
17
  defineResource,
18
+ field,
9
19
  fieldSchema,
10
20
  fileKeyPrefix,
21
+ fileMaxBytes,
22
+ flagEmoji,
23
+ formatPhone,
11
24
  humanize,
25
+ isColor,
26
+ isCountryCode,
27
+ isDomain,
28
+ isPhoneNumber,
29
+ isSlug,
12
30
  kebabCase,
31
+ mimeTypesFor,
32
+ normalizeDomain,
13
33
  pascalCase,
14
34
  pluralize,
35
+ slugify,
15
36
  snakeCase,
37
+ splitPhone,
16
38
  storedFields,
39
+ toE164,
17
40
  words
18
- } from "./chunk-DARDHZPX.js";
41
+ } from "./chunk-434JXSDV.js";
19
42
 
20
43
  // src/password.ts
21
44
  var ALGORITHM = "pbkdf2-sha256";
@@ -466,48 +489,6 @@ function randomLetters(length = 8) {
466
489
  return Array.from(bytes, (byte) => String.fromCharCode(97 + byte % 26)).join("");
467
490
  }
468
491
 
469
- // src/resource/fields.ts
470
- var FILE_CATEGORIES = {
471
- image: ["image/png", "image/jpeg", "image/gif", "image/webp", "image/avif"],
472
- pdf: ["application/pdf"],
473
- video: ["video/*"],
474
- audio: ["audio/*"],
475
- text: ["text/plain"],
476
- csv: ["text/csv"],
477
- document: [
478
- "application/msword",
479
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
480
- "application/vnd.oasis.opendocument.text"
481
- ],
482
- spreadsheet: [
483
- "application/vnd.ms-excel",
484
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
485
- "application/vnd.oasis.opendocument.spreadsheet"
486
- ],
487
- archive: ["application/zip"]
488
- };
489
- function mimeTypesFor(accept) {
490
- return [...new Set(accept.flatMap((category) => FILE_CATEGORIES[category]))];
491
- }
492
- var DEFAULT_FILE_MAX_BYTES = 10 * 1024 * 1024;
493
- var fileMaxBytes = (field2) => field2.maxBytes ?? DEFAULT_FILE_MAX_BYTES;
494
- function build(kind, options, extra = {}) {
495
- return { kind, ...extra, ...options, required: options?.required !== false };
496
- }
497
- var field = {
498
- string: (options) => build("string", options),
499
- text: (options) => build("text", options, { list: false, searchable: false }),
500
- int: (options) => build("int", options),
501
- float: (options) => build("float", options),
502
- boolean: (options) => build("boolean", options),
503
- date: (options) => build("date", options),
504
- datetime: (options) => build("datetime", options),
505
- enum: (values, options) => build("enum", options, { options: values }),
506
- file: (accept, options) => build("file", options, { accept, list: false, sortable: false }),
507
- belongsTo: (target, options) => build("belongsTo", options, { target }),
508
- hasMany: (target, options) => ({ kind: "hasMany", target, ...options, required: false })
509
- };
510
-
511
492
  // src/resource/relations.ts
512
493
  var RelationError = class extends Error {
513
494
  constructor(message) {
@@ -585,7 +566,7 @@ function statusTone(value) {
585
566
  return TONES.find(([, pattern]) => pattern.test(normalized))?.[0] ?? "neutral";
586
567
  }
587
568
  function optionLabel(def, value) {
588
- if (def.kind === "enum" && def.optionLabels?.[value]) return def.optionLabels[value];
569
+ if ((def.kind === "enum" || def.kind === "multiselect") && def.optionLabels?.[value]) return def.optionLabels[value];
589
570
  const text2 = value.replace(/[_-]+/g, " ").trim();
590
571
  return text2 ? text2[0].toUpperCase() + text2.slice(1) : value;
591
572
  }
@@ -610,6 +591,12 @@ function formatValue(def, value, options = {}) {
610
591
  }
611
592
  case "enum":
612
593
  return optionLabel(def, String(value));
594
+ case "multiselect":
595
+ return Array.isArray(value) && value.length ? value.map((item) => optionLabel(def, String(item))).join(", ") : "\u2014";
596
+ case "string":
597
+ if (def.format === "tel") return formatPhone(String(value));
598
+ if (def.format === "country") return `${flagEmoji(String(value))} ${countryName(String(value), locale.split("-")[0])}`.trim();
599
+ return String(value);
613
600
  case "file":
614
601
  return String(value).split("/").pop().replace(/^[0-9a-f-]{36}-/, "");
615
602
  default:
@@ -623,6 +610,7 @@ function initialFormValues(resource, record) {
623
610
  for (const [key, def] of storedFields(resource)) {
624
611
  const source = record ? record[key] : "default" in def ? def.default : void 0;
625
612
  if (def.kind === "boolean") values[key] = source === true || source === 1;
613
+ else if (def.kind === "multiselect") values[key] = JSON.stringify(Array.isArray(source) ? source : []);
626
614
  else if (source === null || source === void 0) values[key] = "";
627
615
  else if (source instanceof Date) values[key] = source.toISOString();
628
616
  else values[key] = String(source);
@@ -637,6 +625,12 @@ function formValuesToInput(resource, values, mode) {
637
625
  input[key] = raw === true;
638
626
  continue;
639
627
  }
628
+ if (def.kind === "multiselect") {
629
+ const picked = parseMultiValue(raw);
630
+ if (picked.length === 0 && mode === "create" && "default" in def && def.default !== void 0) continue;
631
+ input[key] = picked.length === 0 && !def.required ? null : picked;
632
+ continue;
633
+ }
640
634
  const text2 = typeof raw === "string" ? raw : "";
641
635
  const empty = text2.trim() === "";
642
636
  if (empty) {
@@ -655,6 +649,15 @@ function formValuesToInput(resource, values, mode) {
655
649
  }
656
650
  return input;
657
651
  }
652
+ function parseMultiValue(raw) {
653
+ if (typeof raw !== "string" || raw === "") return [];
654
+ try {
655
+ const parsed = JSON.parse(raw);
656
+ return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
657
+ } catch {
658
+ return [];
659
+ }
660
+ }
658
661
  function issuesByField(issues) {
659
662
  const errors = {};
660
663
  for (const issue of issues) {
@@ -665,49 +668,301 @@ function issuesByField(issues) {
665
668
  return errors;
666
669
  }
667
670
 
668
- // src/resource/policy.ts
669
- var ROLE = /^(\*|[a-z][a-z0-9_-]{0,31})$/;
670
- var PolicyError = class extends Error {
671
- constructor(message) {
672
- super(message);
673
- this.name = "PolicyError";
674
- }
675
- };
676
- function definePolicy(config) {
677
- if (!/^[A-Z][A-Za-z0-9]*$/.test(config.resource)) {
678
- throw new PolicyError(`Policy resource "${config.resource}" must be a PascalCase resource name.`);
679
- }
680
- const clean = (action) => {
681
- const roles = config[action] ?? [];
682
- for (const role of roles) {
683
- if (!ROLE.test(role)) throw new PolicyError(`Policy ${config.resource}.${action}: invalid role "${role}" (lowercase letters, digits, _ or -, or "*").`);
684
- }
685
- return [...new Set(roles)];
686
- };
687
- return { resource: config.resource, read: clean("read"), create: clean("create"), update: clean("update"), delete: clean("delete") };
671
+ // src/seed.ts
672
+ function defineSeed(seed) {
673
+ return seed;
688
674
  }
689
- function can(policy, role, action) {
690
- if (!policy) return true;
691
- const allowed = policy[action === "list" ? "read" : action];
692
- if (allowed.includes("*")) return Boolean(role);
693
- return Boolean(role) && allowed.includes(role);
675
+
676
+ // src/fake.ts
677
+ var FIRST_NAMES = [
678
+ "Amina",
679
+ "Daniel",
680
+ "Grace",
681
+ "Wei",
682
+ "Sofia",
683
+ "Tendai",
684
+ "Noah",
685
+ "Priya",
686
+ "Lucas",
687
+ "Fatima",
688
+ "Ibrahim",
689
+ "Elena",
690
+ "Kwame",
691
+ "Maya",
692
+ "Hiroshi",
693
+ "Leila",
694
+ "Marcus",
695
+ "Chiara",
696
+ "Omar",
697
+ "Ana",
698
+ "Joseph",
699
+ "Nadia",
700
+ "Thabo",
701
+ "Yuki",
702
+ "Diego",
703
+ "Ruth",
704
+ "Samuel",
705
+ "Zara",
706
+ "Peter",
707
+ "Isabel",
708
+ "Kofi",
709
+ "Mei",
710
+ "Andr\xE9s",
711
+ "Halima",
712
+ "Jonas",
713
+ "Rania",
714
+ "Esther",
715
+ "Viktor",
716
+ "Aisha",
717
+ "Tom\xE1s"
718
+ ];
719
+ var LAST_NAMES = [
720
+ "Okello",
721
+ "Silva",
722
+ "Nakamura",
723
+ "Mensah",
724
+ "Okafor",
725
+ "Rossi",
726
+ "Chen",
727
+ "Hassan",
728
+ "M\xFCller",
729
+ "Dubois",
730
+ "Kimani",
731
+ "Patel",
732
+ "Novak",
733
+ "Garcia",
734
+ "Haddad",
735
+ "Andersen",
736
+ "Osei",
737
+ "Reyes",
738
+ "Kowalski",
739
+ "Tadesse",
740
+ "Nguyen",
741
+ "Baptiste",
742
+ "Mwangi",
743
+ "Fernandes",
744
+ "Ivanov",
745
+ "Khan",
746
+ "Lindqvist",
747
+ "Santos",
748
+ "Abebe",
749
+ "Yusuf"
750
+ ];
751
+ var COMPANY_HEADS = [
752
+ "North",
753
+ "Blue",
754
+ "Bright",
755
+ "Iron",
756
+ "River",
757
+ "Summit",
758
+ "Atlas",
759
+ "Harbour",
760
+ "Cedar",
761
+ "Copper",
762
+ "Lumen",
763
+ "Orbit",
764
+ "Stone",
765
+ "Vertex",
766
+ "Willow",
767
+ "Zenith",
768
+ "Kestrel",
769
+ "Meridian",
770
+ "Sable",
771
+ "Terra"
772
+ ];
773
+ var COMPANY_TAILS = ["Labs", "Works", "Group", "Studio", "Systems", "Partners", "Supply", "Analytics", "Foods", "Logistics"];
774
+ var CITIES = [
775
+ "Kampala",
776
+ "Nairobi",
777
+ "Lagos",
778
+ "Accra",
779
+ "Cairo",
780
+ "Cape Town",
781
+ "Dar es Salaam",
782
+ "Casablanca",
783
+ "London",
784
+ "Berlin",
785
+ "Madrid",
786
+ "Lisbon",
787
+ "Warsaw",
788
+ "Stockholm",
789
+ "Amsterdam",
790
+ "Dublin",
791
+ "New York",
792
+ "Toronto",
793
+ "Chicago",
794
+ "Austin",
795
+ "S\xE3o Paulo",
796
+ "Bogot\xE1",
797
+ "Mexico City",
798
+ "Lima",
799
+ "Mumbai",
800
+ "Singapore",
801
+ "Tokyo",
802
+ "Seoul",
803
+ "Jakarta",
804
+ "Manila",
805
+ "Sydney",
806
+ "Auckland"
807
+ ];
808
+ var COUNTRIES = [
809
+ ["UG", "256"],
810
+ ["KE", "254"],
811
+ ["NG", "234"],
812
+ ["GH", "233"],
813
+ ["ZA", "27"],
814
+ ["EG", "20"],
815
+ ["US", "1"],
816
+ ["CA", "1"],
817
+ ["GB", "44"],
818
+ ["IE", "353"],
819
+ ["DE", "49"],
820
+ ["FR", "33"],
821
+ ["ES", "34"],
822
+ ["PT", "351"],
823
+ ["NL", "31"],
824
+ ["SE", "46"],
825
+ ["PL", "48"],
826
+ ["BR", "55"],
827
+ ["MX", "52"],
828
+ ["IN", "91"],
829
+ ["SG", "65"],
830
+ ["JP", "81"],
831
+ ["AU", "61"],
832
+ ["AE", "971"]
833
+ ];
834
+ var WORDS = [
835
+ "account",
836
+ "balance",
837
+ "shipment",
838
+ "invoice",
839
+ "review",
840
+ "signal",
841
+ "request",
842
+ "channel",
843
+ "vendor",
844
+ "budget",
845
+ "report",
846
+ "delivery",
847
+ "contract",
848
+ "renewal",
849
+ "forecast",
850
+ "meeting",
851
+ "proposal",
852
+ "discount",
853
+ "refund",
854
+ "ticket",
855
+ "quarter",
856
+ "pipeline",
857
+ "customer",
858
+ "supplier",
859
+ "warehouse",
860
+ "campaign",
861
+ "segment",
862
+ "retention",
863
+ "onboarding",
864
+ "escalation",
865
+ "follow-up",
866
+ "handover",
867
+ "kickoff",
868
+ "milestone",
869
+ "rollout",
870
+ "shortfall",
871
+ "surplus",
872
+ "threshold",
873
+ "variance",
874
+ "workflow"
875
+ ];
876
+ var DOMAINS = ["example.com", "example.org", "example.net", "mail.example.com"];
877
+ var TLDS = ["com", "io", "co", "org", "net"];
878
+ var COLORS = ["#f2541d", "#2563eb", "#16a34a", "#d97706", "#7c3aed", "#0891b2", "#db2777", "#4b5563"];
879
+ var DAY_MS = 864e5;
880
+ var CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
881
+ function encodeCrockford(value, length) {
882
+ let out = "";
883
+ let rest = value;
884
+ for (let i = 0; i < length; i++) {
885
+ out = CROCKFORD[Number(rest % 32n)] + out;
886
+ rest /= 32n;
887
+ }
888
+ return out;
694
889
  }
695
- function allowedActions(policy, role) {
696
- return {
697
- read: can(policy, role, "read"),
698
- create: can(policy, role, "create"),
699
- update: can(policy, role, "update"),
700
- delete: can(policy, role, "delete")
890
+ function random(seed) {
891
+ let state = seed >>> 0;
892
+ return () => {
893
+ state = state + 1831565813 >>> 0;
894
+ let t = Math.imul(state ^ state >>> 15, 1 | state);
895
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
896
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
701
897
  };
702
898
  }
703
-
704
- // src/seed.ts
705
- function defineSeed(seed) {
706
- return seed;
899
+ function createFake(seed = 20260101) {
900
+ const next = random(seed);
901
+ let count = 0;
902
+ const step = () => ++count;
903
+ const tail = BigInt(Math.floor(next() * 2 ** 40)) * 2n ** 40n;
904
+ let counter = 0n;
905
+ const int = (min, max) => min + Math.floor(next() * (max - min + 1));
906
+ const pick = (items) => items[Math.floor(next() * items.length)];
907
+ const slugify2 = (text2) => text2.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
908
+ const fake2 = {
909
+ int,
910
+ float: (min, max, decimals = 2) => Number((min + next() * (max - min)).toFixed(decimals)),
911
+ bool: (chance = 0.5) => next() < chance,
912
+ pick,
913
+ some(items, min = 1, max = items.length) {
914
+ const wanted = int(Math.min(min, items.length), Math.min(max, items.length));
915
+ const chosen = /* @__PURE__ */ new Set();
916
+ while (chosen.size < wanted) chosen.add(Math.floor(next() * items.length));
917
+ return [...chosen].sort((a, b) => a - b).map((index) => items[index]);
918
+ },
919
+ firstName: () => pick(FIRST_NAMES),
920
+ lastName: () => pick(LAST_NAMES),
921
+ fullName: () => `${pick(FIRST_NAMES)} ${pick(LAST_NAMES)}`,
922
+ email(name) {
923
+ const local = name ? slugify2(name).replace(/-/g, ".") : `${pick(FIRST_NAMES)}.${pick(LAST_NAMES)}`.toLowerCase();
924
+ return `${slugify2(local).replace(/-/g, ".")}${step()}@${pick(DOMAINS)}`;
925
+ },
926
+ company: () => `${pick(COMPANY_HEADS)} ${pick(COMPANY_TAILS)}`,
927
+ jobTitle: () => `${pick(["Head of", "Senior", "Lead", "Regional", "Junior"])} ${pick(["Operations", "Sales", "Support", "Engineering", "Finance", "Logistics"])}`,
928
+ city: () => pick(CITIES),
929
+ country: () => pick(COUNTRIES)[0],
930
+ phone: () => `+${pick(COUNTRIES)[1]}${String(int(7e8, 799999999))}`,
931
+ domain: () => `${slugify2(`${pick(COMPANY_HEADS)}${pick(COMPANY_TAILS)}`)}${step()}.${pick(TLDS)}`,
932
+ url: () => `https://${slugify2(pick(COMPANY_HEADS))}.${pick(TLDS)}/${slugify2(pick(WORDS))}`,
933
+ slug: (text2) => `${slugify2(text2 ?? `${pick(WORDS)}-${pick(WORDS)}`)}-${step()}`,
934
+ color: () => pick(COLORS),
935
+ words: (howMany = 3) => Array.from({ length: howMany }, () => pick(WORDS)).join(" "),
936
+ sentence() {
937
+ const words2 = Array.from({ length: int(5, 12) }, () => pick(WORDS)).join(" ");
938
+ return `${words2[0].toUpperCase()}${words2.slice(1)}.`;
939
+ },
940
+ paragraph(sentences = 3) {
941
+ return Array.from({ length: sentences }, () => fake2.sentence()).join(" ");
942
+ },
943
+ timestamp: (daysBack = 365) => Date.now() - int(0, daysBack) * DAY_MS - int(0, DAY_MS - 1),
944
+ date: (daysBack = 365) => new Date(fake2.timestamp(daysBack)).toISOString().slice(0, 10),
945
+ datetime: (daysBack = 365) => new Date(fake2.timestamp(daysBack)).toISOString(),
946
+ uuid() {
947
+ const hex = "0123456789abcdef";
948
+ let out = "";
949
+ for (let i = 0; i < 32; i++) out += hex[Math.floor(next() * 16)];
950
+ return `${out.slice(0, 8)}-${out.slice(8, 12)}-4${out.slice(13, 16)}-a${out.slice(17, 20)}-${out.slice(20, 32)}`;
951
+ },
952
+ id() {
953
+ counter += 1n;
954
+ return encodeCrockford(BigInt(Date.now()), 10) + encodeCrockford((tail + counter) % 2n ** 80n, 16);
955
+ },
956
+ get count() {
957
+ return count;
958
+ }
959
+ };
960
+ return fake2;
707
961
  }
962
+ var fake = createFake();
708
963
 
709
964
  // src/index.ts
710
- var FLARE_VERSION = "0.1.2";
965
+ var FLARE_VERSION = "0.3.0";
711
966
  export {
712
967
  DEFAULT_FILE_MAX_BYTES,
713
968
  FILE_CATEGORIES,
@@ -718,12 +973,16 @@ export {
718
973
  RelationError,
719
974
  ResourceDefinitionError,
720
975
  SNIFF_BYTES,
976
+ STRING_FORMATS,
721
977
  SUBSCRIPTION_STATUSES,
722
978
  allowedActions,
723
979
  camelCase,
724
980
  can,
725
981
  checkoutPaid,
726
982
  columnName,
983
+ countries,
984
+ countryName,
985
+ createFake,
727
986
  createMailer,
728
987
  createObjectKey,
729
988
  createStorage,
@@ -733,23 +992,33 @@ export {
733
992
  definePolicy,
734
993
  defineResource,
735
994
  defineSeed,
995
+ fake,
736
996
  field,
737
997
  fieldSchema,
738
998
  fileKeyPrefix,
739
999
  fileMaxBytes,
1000
+ flagEmoji,
740
1001
  formValuesToInput,
1002
+ formatPhone,
741
1003
  formatValue,
742
1004
  grantsAccess,
743
1005
  hashPassword,
744
1006
  humanize,
745
1007
  importSigningKey,
746
1008
  initialFormValues,
1009
+ isColor,
1010
+ isCountryCode,
1011
+ isDomain,
747
1012
  isLiveSubscription,
1013
+ isPhoneNumber,
1014
+ isSlug,
748
1015
  issuesByField,
749
1016
  kebabCase,
750
1017
  matchesContentType,
751
1018
  mimeTypesFor,
1019
+ normalizeDomain,
752
1020
  optionLabel,
1021
+ parseMultiValue,
753
1022
  pascalCase,
754
1023
  pluralize,
755
1024
  randomLetters,
@@ -757,12 +1026,15 @@ export {
757
1026
  renderTransactionalEmail,
758
1027
  shouldApplySubscription,
759
1028
  signToken,
1029
+ slugify,
760
1030
  snakeCase,
761
1031
  sniffMatches,
1032
+ splitPhone,
762
1033
  statusTone,
763
1034
  storedFields,
764
1035
  subscriptionPriceId,
765
1036
  subscriptionState,
1037
+ toE164,
766
1038
  toSubscriptionStatus,
767
1039
  verifyPassword,
768
1040
  verifyToken,
@@ -6,7 +6,7 @@
6
6
  * rule in a descriptor takes effect without re-running the generator. Only
7
7
  * storage-level changes (kind, nullability, uniqueness, relations) need a migration.
8
8
  */
9
- type FieldKind = "string" | "text" | "int" | "float" | "boolean" | "date" | "datetime" | "enum" | "file" | "belongsTo" | "hasMany";
9
+ type FieldKind = "string" | "text" | "int" | "float" | "boolean" | "date" | "datetime" | "enum" | "multiselect" | "file" | "belongsTo" | "hasMany";
10
10
  /** Options shared by every stored (non-hasMany) field. */
11
11
  interface CommonOptions<T> {
12
12
  label?: string;
@@ -24,10 +24,23 @@ interface CommonOptions<T> {
24
24
  /** Included in free-text search. Default true for string fields. */
25
25
  searchable?: boolean;
26
26
  }
27
+ /**
28
+ * What a string holds. Each format is still a text column; the format decides
29
+ * validation, the admin input and how the value is shown.
30
+ *
31
+ * - email, url: validated addresses (url: http or https)
32
+ * - tel: an international number, stored E.164 (`+256772123456`), entered with a country picker
33
+ * - domain: a hostname such as `example.com`, stored lowercase
34
+ * - country: an ISO 3166-1 alpha-2 code (`UG`), chosen from a searchable list
35
+ * - color: a hex colour (`#f2541d`)
36
+ * - slug: lowercase words joined by hyphens (`my-first-post`)
37
+ */
38
+ type StringFormat = "email" | "url" | "tel" | "domain" | "country" | "color" | "slug";
39
+ declare const STRING_FORMATS: readonly StringFormat[];
27
40
  interface StringOptions extends CommonOptions<string> {
28
41
  maxLength?: number;
29
42
  minLength?: number;
30
- format?: "email" | "url";
43
+ format?: StringFormat;
31
44
  pattern?: string;
32
45
  }
33
46
  interface TextOptions extends CommonOptions<string> {
@@ -43,6 +56,14 @@ type DateOptions = CommonOptions<string>;
43
56
  interface EnumOptions<O extends string> extends CommonOptions<O> {
44
57
  /** Display labels per option value. */
45
58
  optionLabels?: Partial<Record<O, string>>;
59
+ /** Admin input: a dropdown (default) or radio buttons (best for a few options). */
60
+ widget?: "select" | "radio";
61
+ }
62
+ interface MultiSelectOptions<O extends string> extends Omit<CommonOptions<O[]>, "unique"> {
63
+ optionLabels?: Partial<Record<O, string>>;
64
+ /** Fewest / most options that can be picked. A required field needs at least one. */
65
+ minItems?: number;
66
+ maxItems?: number;
46
67
  }
47
68
  interface FileOptions extends CommonOptions<string> {
48
69
  /** Maximum upload size in bytes. Default 10 MiB. */
@@ -75,6 +96,9 @@ type DateTimeField<R extends boolean = boolean> = Base<"datetime", R> & DateOpti
75
96
  type EnumField<O extends string = string, R extends boolean = boolean> = Base<"enum", R> & EnumOptions<O> & {
76
97
  options: readonly O[];
77
98
  };
99
+ type MultiSelectField<O extends string = string, R extends boolean = boolean> = Base<"multiselect", R> & MultiSelectOptions<O> & {
100
+ options: readonly O[];
101
+ };
78
102
  type FileField<R extends boolean = boolean> = Base<"file", R> & FileOptions & {
79
103
  /** Accepted categories from the `file:[image,pdf]` grammar. */
80
104
  accept: readonly FileCategory[];
@@ -88,7 +112,7 @@ type HasManyField = {
88
112
  required: false;
89
113
  target: string;
90
114
  } & HasManyOptions;
91
- type Field = StringField | TextField | IntField | FloatField | BooleanField | DateField | DateTimeField | EnumField | FileField | BelongsToField | HasManyField;
115
+ type Field = StringField | TextField | IntField | FloatField | BooleanField | DateField | DateTimeField | EnumField | MultiSelectField | FileField | BelongsToField | HasManyField;
92
116
  type StoredField = Exclude<Field, HasManyField>;
93
117
  /** File categories usable in `file:[...]` and the MIME types each allows. */
94
118
  declare const FILE_CATEGORIES: {
@@ -119,6 +143,19 @@ declare const field: {
119
143
  date: <const O extends DateOptions>(options?: O) => DateField<Req<O>> & O;
120
144
  datetime: <const O extends DateOptions>(options?: O) => DateTimeField<Req<O>> & O;
121
145
  enum: <const V extends readonly [string, ...string[]], const O extends EnumOptions<V[number]>>(values: V, options?: O) => EnumField<V[number], Req<O>> & O;
146
+ /** One of a list, picked from a dropdown. The same as `enum`. */
147
+ select: <const V extends readonly [string, ...string[]], const O extends EnumOptions<V[number]>>(values: V, options?: O) => EnumField<V[number], Req<O>> & O;
148
+ /** One of a list, picked with radio buttons. Stored like `enum`. */
149
+ radio: <const V extends readonly [string, ...string[]], const O extends EnumOptions<V[number]>>(values: V, options?: O) => EnumField<V[number], Req<O>> & O;
150
+ /** Any number of a list, picked with checkboxes. Stored as a JSON array. */
151
+ multiselect: <const V extends readonly [string, ...string[]], const O extends MultiSelectOptions<V[number]>>(values: V, options?: O) => MultiSelectField<V[number], Req<O>> & O;
152
+ email: <const O extends Omit<StringOptions, "format">>(options?: O) => StringField<Req<O>> & O;
153
+ url: <const O extends Omit<StringOptions, "format">>(options?: O) => StringField<Req<O>> & O;
154
+ tel: <const O extends Omit<StringOptions, "format">>(options?: O) => StringField<Req<O>> & O;
155
+ domain: <const O extends Omit<StringOptions, "format">>(options?: O) => StringField<Req<O>> & O;
156
+ country: <const O extends Omit<StringOptions, "format">>(options?: O) => StringField<Req<O>> & O;
157
+ color: <const O extends Omit<StringOptions, "format">>(options?: O) => StringField<Req<O>> & O;
158
+ slug: <const O extends Omit<StringOptions, "format">>(options?: O) => StringField<Req<O>> & O;
122
159
  file: <const O extends FileOptions>(accept: readonly FileCategory[], options?: O) => FileField<Req<O>> & O;
123
160
  belongsTo: <const O extends BelongsToOptions>(target: string, options?: O) => BelongsToField<Req<O>> & O;
124
161
  hasMany: <const O extends HasManyOptions>(target: string, options?: O) => HasManyField & O;
@@ -131,6 +168,9 @@ type FieldValue<F> = F extends {
131
168
  kind: "enum";
132
169
  options: readonly (infer O)[];
133
170
  } ? O : F extends {
171
+ kind: "multiselect";
172
+ options: readonly (infer M)[];
173
+ } ? M[] : F extends {
134
174
  kind: "hasMany";
135
175
  } ? never : string;
136
176
  type StoredKeys<Fields> = {
@@ -229,4 +269,40 @@ declare function storedFields(resource: Resource): [string, StoredField & {
229
269
  /** Database column name for a field key ("companyId" → "company_id"). */
230
270
  declare const columnName: (key: string) => string;
231
271
 
232
- export { type BelongsToField as B, type CreateInput as C, DEFAULT_FILE_MAX_BYTES as D, type EnumField as E, type Field as F, type HasManyField as H, type IntField as I, type Resource as R, type StoredField as S, type TextField as T, type UpdateInput as U, type BooleanField as a, type DateField as b, type DateTimeField as c, FILE_CATEGORIES as d, type FieldKind as e, type FieldValue as f, type FileCategory as g, type FileField as h, type FloatField as i, RESERVED_FIELD_NAMES as j, type RecordOf as k, type ResourceConfig as l, ResourceDefinitionError as m, type StringField as n, columnName as o, defaultFieldLabel as p, defineResource as q, field as r, fileMaxBytes as s, mimeTypesFor as t, storedFields as u };
272
+ /**
273
+ * Resource-level policies: which roles may read, create, update, or delete a resource.
274
+ *
275
+ * Policies are plain data, like descriptors, so the API layer and the admin UI read the
276
+ * same rules at runtime. Field-level rules and per-record ownership are deliberately out
277
+ * of scope for v1.
278
+ */
279
+ type PolicyAction = "list" | "read" | "create" | "update" | "delete";
280
+ /** Roles allowed per action; `"*"` means any signed-in user. */
281
+ interface Policy {
282
+ resource: string;
283
+ read: string[];
284
+ create: string[];
285
+ update: string[];
286
+ delete: string[];
287
+ }
288
+ interface PolicyConfig {
289
+ resource: string;
290
+ read?: string[];
291
+ create?: string[];
292
+ update?: string[];
293
+ delete?: string[];
294
+ }
295
+ declare class PolicyError extends Error {
296
+ constructor(message: string);
297
+ }
298
+ declare function definePolicy(config: PolicyConfig): Policy;
299
+ /**
300
+ * Whether `role` may perform `action`. Without a policy any signed-in user is allowed
301
+ * (callers must already have required a session); generate one with
302
+ * `flare gen policy <Resource> --roles …` to restrict a resource.
303
+ */
304
+ declare function can(policy: Policy | undefined | null, role: string | null | undefined, action: PolicyAction): boolean;
305
+ /** Actions `role` may perform, for hiding UI it can't use. */
306
+ declare function allowedActions(policy: Policy | undefined | null, role: string | null | undefined): Record<Exclude<PolicyAction, "list">, boolean>;
307
+
308
+ export { fileMaxBytes as A, type BelongsToField as B, type CreateInput as C, DEFAULT_FILE_MAX_BYTES as D, type EnumField as E, type Field as F, mimeTypesFor as G, type HasManyField as H, type IntField as I, storedFields as J, type MultiSelectField as M, type Policy as P, type Resource as R, type StoredField as S, type TextField as T, type UpdateInput as U, type BooleanField as a, type DateField as b, type DateTimeField as c, FILE_CATEGORIES as d, type FieldKind as e, type FieldValue as f, type FileCategory as g, type FileField as h, type FloatField as i, type PolicyAction as j, type PolicyConfig as k, PolicyError as l, RESERVED_FIELD_NAMES as m, type RecordOf as n, type ResourceConfig as o, ResourceDefinitionError as p, STRING_FORMATS as q, type StringField as r, type StringFormat as s, allowedActions as t, can as u, columnName as v, defaultFieldLabel as w, definePolicy as x, defineResource as y, field as z };