@flaredev/core 0.1.2 → 0.2.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,13 @@ 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") };
688
- }
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);
694
- }
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")
701
- };
702
- }
703
-
704
671
  // src/seed.ts
705
672
  function defineSeed(seed) {
706
673
  return seed;
707
674
  }
708
675
 
709
676
  // src/index.ts
710
- var FLARE_VERSION = "0.1.2";
677
+ var FLARE_VERSION = "0.2.0";
711
678
  export {
712
679
  DEFAULT_FILE_MAX_BYTES,
713
680
  FILE_CATEGORIES,
@@ -718,12 +685,15 @@ export {
718
685
  RelationError,
719
686
  ResourceDefinitionError,
720
687
  SNIFF_BYTES,
688
+ STRING_FORMATS,
721
689
  SUBSCRIPTION_STATUSES,
722
690
  allowedActions,
723
691
  camelCase,
724
692
  can,
725
693
  checkoutPaid,
726
694
  columnName,
695
+ countries,
696
+ countryName,
727
697
  createMailer,
728
698
  createObjectKey,
729
699
  createStorage,
@@ -737,19 +707,28 @@ export {
737
707
  fieldSchema,
738
708
  fileKeyPrefix,
739
709
  fileMaxBytes,
710
+ flagEmoji,
740
711
  formValuesToInput,
712
+ formatPhone,
741
713
  formatValue,
742
714
  grantsAccess,
743
715
  hashPassword,
744
716
  humanize,
745
717
  importSigningKey,
746
718
  initialFormValues,
719
+ isColor,
720
+ isCountryCode,
721
+ isDomain,
747
722
  isLiveSubscription,
723
+ isPhoneNumber,
724
+ isSlug,
748
725
  issuesByField,
749
726
  kebabCase,
750
727
  matchesContentType,
751
728
  mimeTypesFor,
729
+ normalizeDomain,
752
730
  optionLabel,
731
+ parseMultiValue,
753
732
  pascalCase,
754
733
  pluralize,
755
734
  randomLetters,
@@ -757,12 +736,15 @@ export {
757
736
  renderTransactionalEmail,
758
737
  shouldApplySubscription,
759
738
  signToken,
739
+ slugify,
760
740
  snakeCase,
761
741
  sniffMatches,
742
+ splitPhone,
762
743
  statusTone,
763
744
  storedFields,
764
745
  subscriptionPriceId,
765
746
  subscriptionState,
747
+ toE164,
766
748
  toSubscriptionStatus,
767
749
  verifyPassword,
768
750
  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 };
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 } from 'drizzle-orm/sqlite-core';
3
- import { S as StoredField, R as Resource, F as Field } from './define-D5CtCkOm.js';
3
+ import { S as StoredField, R as Resource, F as Field, P as Policy } from './policy-BqRg9RVM.js';
4
4
 
5
5
  interface ListQuery {
6
6
  page: number;
@@ -153,4 +153,31 @@ declare function createResourceHandlers(options: ResourceHandlerOptions): {
153
153
  };
154
154
  };
155
155
 
156
- export { type AnyDatabase, type Authorize, type AuthorizeContext, type ChangeEvent, type Failure, type FieldIssue, type ListQuery, type ListResult, type QueryIssue, type ResourceAction, type ResourceHandlerOptions, type ResourceStore, type ResourceStoreOptions, type Result, createResourceHandlers, createResourceStore, isFilterable, isSearchable, isSortable, parseListQuery };
156
+ /**
157
+ * An OpenAPI 3.1 document for an app's generated REST API, built at request time
158
+ * from the same descriptors and policies the API itself runs on, so it can't drift.
159
+ *
160
+ * Operations are filtered by role: a signed-in user sees what their role may call
161
+ * (admins everything), a visitor only the sign-in endpoints. `visibility: "public"`
162
+ * lists every operation to everyone.
163
+ */
164
+
165
+ type Schema = Record<string, unknown>;
166
+ interface OpenApiOptions {
167
+ resources: readonly Resource[];
168
+ policies?: Record<string, Policy | undefined>;
169
+ /** The viewer's role; null for a signed-out visitor. */
170
+ role: string | null;
171
+ /** "role" (default): only operations the viewer's role may call. "public": all of them. */
172
+ visibility?: "role" | "public";
173
+ title?: string;
174
+ version?: string;
175
+ description?: string;
176
+ /** Absolute origin the "Try it" requests go to, e.g. https://app.example.com. */
177
+ serverUrl?: string;
178
+ }
179
+ /** JSON Schema for one field value (never null; nullability is added by the caller). */
180
+ declare function fieldJsonSchema(def: StoredField): Schema;
181
+ declare function openApiDocument(options: OpenApiOptions): Schema;
182
+
183
+ export { type AnyDatabase, type Authorize, type AuthorizeContext, type ChangeEvent, type Failure, type FieldIssue, type ListQuery, type ListResult, type OpenApiOptions, type QueryIssue, type ResourceAction, type ResourceHandlerOptions, type ResourceStore, type ResourceStoreOptions, type Result, createResourceHandlers, createResourceStore, fieldJsonSchema, isFilterable, isSearchable, isSortable, openApiDocument, parseListQuery };