@flaredev/core 0.8.1 → 0.9.1
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.
|
@@ -15,6 +15,7 @@ var STRING_FORMATS = [
|
|
|
15
15
|
"currency",
|
|
16
16
|
"postcode"
|
|
17
17
|
];
|
|
18
|
+
var TEXT_FORMATS = ["markdown"];
|
|
18
19
|
var NUMBER_FORMATS = ["money", "percent", "rating"];
|
|
19
20
|
var FILE_CATEGORIES = {
|
|
20
21
|
// For a field that holds whatever someone has: a drive, an attachment, a backup.
|
|
@@ -72,6 +73,12 @@ var field = {
|
|
|
72
73
|
radio: (values, options) => build("enum", options, { options: values, widget: "radio" }),
|
|
73
74
|
/** Any number of a list, picked with checkboxes. Stored as a JSON array. */
|
|
74
75
|
multiselect: (values, options) => build("multiselect", options, { options: values, sortable: false }),
|
|
76
|
+
/** Free-text labels, as many as wanted. Stored as a JSON array. */
|
|
77
|
+
tags: (options) => build("tags", options, { sortable: false, searchable: false }),
|
|
78
|
+
/** Anything not table-shaped: a settings blob, a payload, metadata. Stored as JSON. */
|
|
79
|
+
json: (options) => build("json", options, { list: false, sortable: false, searchable: false }),
|
|
80
|
+
/** Long text written as Markdown: a split editor, rendered where it is shown. */
|
|
81
|
+
markdown: (options) => build("text", options, { format: "markdown", list: false, searchable: false }),
|
|
75
82
|
email: (options) => build("string", options, { format: "email" }),
|
|
76
83
|
url: (options) => build("string", options, { format: "url" }),
|
|
77
84
|
tel: (options) => build("string", options, { format: "tel" }),
|
|
@@ -116,7 +123,7 @@ function pluralize(value) {
|
|
|
116
123
|
}
|
|
117
124
|
|
|
118
125
|
// src/resource/define.ts
|
|
119
|
-
var RESERVED_FIELD_NAMES = ["id", "createdAt", "updatedAt"];
|
|
126
|
+
var RESERVED_FIELD_NAMES = ["id", "createdAt", "updatedAt", "deletedAt"];
|
|
120
127
|
function clientResource(resource) {
|
|
121
128
|
const { hooks, computed, $types, ...rest } = resource;
|
|
122
129
|
void hooks;
|
|
@@ -195,6 +202,7 @@ function defineResource(config) {
|
|
|
195
202
|
titleField,
|
|
196
203
|
defaultSort,
|
|
197
204
|
perPage,
|
|
205
|
+
softDelete: config.softDelete === true,
|
|
198
206
|
fields,
|
|
199
207
|
$types: void 0
|
|
200
208
|
};
|
|
@@ -379,6 +387,19 @@ function fieldSchema(def) {
|
|
|
379
387
|
if (def.maxItems !== void 0) s = s.refine((values) => values.length <= def.maxItems, `Choose at most ${def.maxItems}`);
|
|
380
388
|
return s;
|
|
381
389
|
}
|
|
390
|
+
case "tags": {
|
|
391
|
+
const maxLength = def.maxLength ?? 32;
|
|
392
|
+
let s = z.array(
|
|
393
|
+
z.string({ error: "Each tag is text" }).trim().min(1, "A tag can't be blank").max(maxLength, `A tag can be at most ${maxLength} characters`),
|
|
394
|
+
{ error: typeError("Add one or more tags") }
|
|
395
|
+
).refine((values) => new Set(values.map((value) => value.toLowerCase())).size === values.length, "Each tag can be added once");
|
|
396
|
+
const min = def.minItems ?? (def.required ? 1 : 0);
|
|
397
|
+
if (min > 0) s = s.refine((values) => values.length >= min, min === 1 ? "Add at least one tag" : `Add at least ${min} tags`);
|
|
398
|
+
if (def.maxItems !== void 0) s = s.refine((values) => values.length <= def.maxItems, `Add at most ${def.maxItems} tags`);
|
|
399
|
+
return s;
|
|
400
|
+
}
|
|
401
|
+
case "json":
|
|
402
|
+
return def.object ? z.record(z.string(), z.json(), { error: typeError("Enter a JSON object") }) : z.json();
|
|
382
403
|
case "file":
|
|
383
404
|
return z.string({ error: typeError("Upload a file") }).regex(OBJECT_KEY, "Invalid file");
|
|
384
405
|
case "belongsTo":
|
|
@@ -412,6 +433,7 @@ function createValidators(resource) {
|
|
|
412
433
|
|
|
413
434
|
// src/resource/policy.ts
|
|
414
435
|
var ROLE = /^(\*|[a-z][a-z0-9_-]{0,31})$/;
|
|
436
|
+
var FIELD = /^[a-z][A-Za-z0-9]*$/;
|
|
415
437
|
var PolicyError = class extends Error {
|
|
416
438
|
constructor(message) {
|
|
417
439
|
super(message);
|
|
@@ -429,7 +451,25 @@ function definePolicy(config) {
|
|
|
429
451
|
}
|
|
430
452
|
return [...new Set(roles)];
|
|
431
453
|
};
|
|
432
|
-
|
|
454
|
+
const policy = {
|
|
455
|
+
resource: config.resource,
|
|
456
|
+
read: clean("read"),
|
|
457
|
+
create: clean("create"),
|
|
458
|
+
update: clean("update"),
|
|
459
|
+
delete: clean("delete")
|
|
460
|
+
};
|
|
461
|
+
if (config.own) {
|
|
462
|
+
const { field: field2, except = [] } = config.own;
|
|
463
|
+
if (!FIELD.test(field2)) {
|
|
464
|
+
throw new PolicyError(`Policy ${config.resource}.own: "${field2}" is not a field name (camelCase, starting with a lowercase letter).`);
|
|
465
|
+
}
|
|
466
|
+
for (const role of except) {
|
|
467
|
+
if (role === "*") throw new PolicyError(`Policy ${config.resource}.own.except: "*" exempts everyone, which is the same as leaving out \`own\`.`);
|
|
468
|
+
if (!ROLE.test(role)) throw new PolicyError(`Policy ${config.resource}.own.except: invalid role "${role}" (lowercase letters, digits, _ or -).`);
|
|
469
|
+
}
|
|
470
|
+
policy.own = { field: field2, except: [...new Set(except)] };
|
|
471
|
+
}
|
|
472
|
+
return policy;
|
|
433
473
|
}
|
|
434
474
|
function can(policy, role, action) {
|
|
435
475
|
if (!policy) return true;
|
|
@@ -437,6 +477,12 @@ function can(policy, role, action) {
|
|
|
437
477
|
if (allowed.includes("*")) return Boolean(role);
|
|
438
478
|
return Boolean(role) && allowed.includes(role);
|
|
439
479
|
}
|
|
480
|
+
function ownershipFilter(policy, user) {
|
|
481
|
+
const own = policy?.own;
|
|
482
|
+
if (!own || !user) return null;
|
|
483
|
+
if (user.role != null && own.except.includes(user.role)) return null;
|
|
484
|
+
return { field: own.field, value: user.id };
|
|
485
|
+
}
|
|
440
486
|
function allowedActions(policy, role) {
|
|
441
487
|
return {
|
|
442
488
|
read: can(policy, role, "read"),
|
|
@@ -448,6 +494,7 @@ function allowedActions(policy, role) {
|
|
|
448
494
|
|
|
449
495
|
export {
|
|
450
496
|
STRING_FORMATS,
|
|
497
|
+
TEXT_FORMATS,
|
|
451
498
|
NUMBER_FORMATS,
|
|
452
499
|
FILE_CATEGORIES,
|
|
453
500
|
mimeTypesFor,
|
|
@@ -487,5 +534,6 @@ export {
|
|
|
487
534
|
PolicyError,
|
|
488
535
|
definePolicy,
|
|
489
536
|
can,
|
|
537
|
+
ownershipFilter,
|
|
490
538
|
allowedActions
|
|
491
539
|
};
|
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-
|
|
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, M as MultiSelectField, N as NUMBER_FORMATS,
|
|
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';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -375,7 +375,7 @@ declare function formatValue(def: StoredField | {
|
|
|
375
375
|
|
|
376
376
|
/**
|
|
377
377
|
* Form state for a resource: strings for text-like inputs, booleans for toggles, and a
|
|
378
|
-
* JSON array string for multiselects.
|
|
378
|
+
* JSON array string for multiselects and tags.
|
|
379
379
|
* These helpers convert between records, form state, and API input so every admin
|
|
380
380
|
* form handles empty values, numbers, and defaults the same way.
|
|
381
381
|
*/
|
|
@@ -388,7 +388,7 @@ declare function initialFormValues(resource: Pick<Resource, "fields">, record?:
|
|
|
388
388
|
* empty required fields are left for validation to report as "Required".
|
|
389
389
|
*/
|
|
390
390
|
declare function formValuesToInput(resource: Pick<Resource, "fields">, values: FormValues, mode: "create" | "edit"): Record<string, unknown>;
|
|
391
|
-
/** A multiselect's form state (a JSON array string) as the picked values. */
|
|
391
|
+
/** A multiselect's or tags field's form state (a JSON array string) as the picked values. */
|
|
392
392
|
declare function parseMultiValue(raw: string | boolean | undefined): string[];
|
|
393
393
|
/** Field errors keyed by field name, from validation or API issues (first message wins). */
|
|
394
394
|
declare function issuesByField(issues: {
|
|
@@ -529,6 +529,6 @@ interface SeedContext {
|
|
|
529
529
|
type Seed = (context: SeedContext) => void | Promise<void>;
|
|
530
530
|
declare function defineSeed(seed: Seed): Seed;
|
|
531
531
|
|
|
532
|
-
declare const FLARE_VERSION = "0.
|
|
532
|
+
declare const FLARE_VERSION = "0.9.1";
|
|
533
533
|
|
|
534
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 };
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
RESERVED_FIELD_NAMES,
|
|
7
7
|
ResourceDefinitionError,
|
|
8
8
|
STRING_FORMATS,
|
|
9
|
+
TEXT_FORMATS,
|
|
9
10
|
allowedActions,
|
|
10
11
|
camelCase,
|
|
11
12
|
can,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
kebabCase,
|
|
33
34
|
mimeTypesFor,
|
|
34
35
|
normalizeDomain,
|
|
36
|
+
ownershipFilter,
|
|
35
37
|
pascalCase,
|
|
36
38
|
pluralize,
|
|
37
39
|
slugify,
|
|
@@ -40,7 +42,7 @@ import {
|
|
|
40
42
|
storedFields,
|
|
41
43
|
toE164,
|
|
42
44
|
words
|
|
43
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-AAXNSIZG.js";
|
|
44
46
|
|
|
45
47
|
// src/password.ts
|
|
46
48
|
var ALGORITHM = "pbkdf2-sha256";
|
|
@@ -605,6 +607,13 @@ function formatValue(def, value, options = {}) {
|
|
|
605
607
|
return optionLabel(def, String(value));
|
|
606
608
|
case "multiselect":
|
|
607
609
|
return Array.isArray(value) && value.length ? value.map((item) => optionLabel(def, String(item))).join(", ") : "\u2014";
|
|
610
|
+
case "tags":
|
|
611
|
+
return Array.isArray(value) && value.length ? value.map(String).join(", ") : "\u2014";
|
|
612
|
+
case "json": {
|
|
613
|
+
if (value === null || value === void 0) return "\u2014";
|
|
614
|
+
const text2 = typeof value === "string" ? value : JSON.stringify(value);
|
|
615
|
+
return text2.length > 60 ? `${text2.slice(0, 59)}\u2026` : text2;
|
|
616
|
+
}
|
|
608
617
|
case "string":
|
|
609
618
|
if (def.format === "tel") return formatPhone(String(value));
|
|
610
619
|
if (def.format === "country") return `${flagEmoji(String(value))} ${countryName(String(value), locale.split("-")[0])}`.trim();
|
|
@@ -622,7 +631,8 @@ function initialFormValues(resource, record) {
|
|
|
622
631
|
for (const [key, def] of storedFields(resource)) {
|
|
623
632
|
const source = record ? record[key] : "default" in def ? def.default : void 0;
|
|
624
633
|
if (def.kind === "boolean") values[key] = source === true || source === 1;
|
|
625
|
-
else if (def.kind === "multiselect") values[key] = JSON.stringify(Array.isArray(source) ? source : []);
|
|
634
|
+
else if (def.kind === "multiselect" || def.kind === "tags") values[key] = JSON.stringify(Array.isArray(source) ? source : []);
|
|
635
|
+
else if (def.kind === "json") values[key] = source === null || source === void 0 ? "" : JSON.stringify(source, null, 2);
|
|
626
636
|
else if (source === null || source === void 0) values[key] = "";
|
|
627
637
|
else if (source instanceof Date) values[key] = source.toISOString();
|
|
628
638
|
else values[key] = String(source);
|
|
@@ -637,12 +647,26 @@ function formValuesToInput(resource, values, mode) {
|
|
|
637
647
|
input[key] = raw === true;
|
|
638
648
|
continue;
|
|
639
649
|
}
|
|
640
|
-
if (def.kind === "multiselect") {
|
|
650
|
+
if (def.kind === "multiselect" || def.kind === "tags") {
|
|
641
651
|
const picked = parseMultiValue(raw);
|
|
642
652
|
if (picked.length === 0 && mode === "create" && "default" in def && def.default !== void 0) continue;
|
|
643
653
|
input[key] = picked.length === 0 && !def.required ? null : picked;
|
|
644
654
|
continue;
|
|
645
655
|
}
|
|
656
|
+
if (def.kind === "json") {
|
|
657
|
+
const source = typeof raw === "string" ? raw.trim() : "";
|
|
658
|
+
if (source === "") {
|
|
659
|
+
if (mode === "create" && "default" in def && def.default !== void 0) continue;
|
|
660
|
+
if (!def.required) input[key] = null;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
try {
|
|
664
|
+
input[key] = JSON.parse(source);
|
|
665
|
+
} catch {
|
|
666
|
+
input[key] = Number.NaN;
|
|
667
|
+
}
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
646
670
|
const text2 = typeof raw === "string" ? raw : "";
|
|
647
671
|
const empty = text2.trim() === "";
|
|
648
672
|
if (empty) {
|
|
@@ -995,7 +1019,7 @@ function createFake(seed = 20260101) {
|
|
|
995
1019
|
var fake = createFake();
|
|
996
1020
|
|
|
997
1021
|
// src/index.ts
|
|
998
|
-
var FLARE_VERSION = "0.
|
|
1022
|
+
var FLARE_VERSION = "0.9.1";
|
|
999
1023
|
export {
|
|
1000
1024
|
DEFAULT_FILE_MAX_BYTES,
|
|
1001
1025
|
FILE_CATEGORIES,
|
|
@@ -1009,6 +1033,7 @@ export {
|
|
|
1009
1033
|
SNIFF_BYTES,
|
|
1010
1034
|
STRING_FORMATS,
|
|
1011
1035
|
SUBSCRIPTION_STATUSES,
|
|
1036
|
+
TEXT_FORMATS,
|
|
1012
1037
|
allowedActions,
|
|
1013
1038
|
camelCase,
|
|
1014
1039
|
can,
|
|
@@ -1053,6 +1078,7 @@ export {
|
|
|
1053
1078
|
mimeTypesFor,
|
|
1054
1079
|
normalizeDomain,
|
|
1055
1080
|
optionLabel,
|
|
1081
|
+
ownershipFilter,
|
|
1056
1082
|
parseMultiValue,
|
|
1057
1083
|
pascalCase,
|
|
1058
1084
|
pluralize,
|
|
@@ -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" | "multiselect" | "file" | "belongsTo" | "hasMany";
|
|
9
|
+
type FieldKind = "string" | "text" | "int" | "float" | "boolean" | "date" | "datetime" | "enum" | "multiselect" | "tags" | "json" | "file" | "belongsTo" | "hasMany";
|
|
10
10
|
/** Options shared by every stored (non-hasMany) field. */
|
|
11
11
|
interface CommonOptions<T> {
|
|
12
12
|
label?: string;
|
|
@@ -50,9 +50,49 @@ interface StringOptions extends CommonOptions<string> {
|
|
|
50
50
|
format?: StringFormat;
|
|
51
51
|
pattern?: string;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* What long text holds.
|
|
55
|
+
*
|
|
56
|
+
* The column is `text` either way. `markdown` changes the editor — a split write/preview
|
|
57
|
+
* pane rather than a plain textarea — and how the value is rendered. Nothing is escaped
|
|
58
|
+
* or sanitised on the way in: it is stored exactly as typed, and rendered as Markdown
|
|
59
|
+
* where the dashboard shows it.
|
|
60
|
+
*/
|
|
61
|
+
type TextFormat = "markdown";
|
|
62
|
+
declare const TEXT_FORMATS: readonly TextFormat[];
|
|
53
63
|
interface TextOptions extends CommonOptions<string> {
|
|
54
64
|
maxLength?: number;
|
|
55
65
|
minLength?: number;
|
|
66
|
+
format?: TextFormat;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Free-text labels, as many as you like.
|
|
70
|
+
*
|
|
71
|
+
* `multiselect` is the one to reach for when the vocabulary is fixed and known — it
|
|
72
|
+
* validates against its options and renders a picker. `tags` is for the other case: the
|
|
73
|
+
* person typing decides what the values are. Stored as a JSON array, like a multiselect,
|
|
74
|
+
* so a row holds `["urgent", "q3"]` rather than a comma-joined string nobody can query.
|
|
75
|
+
*/
|
|
76
|
+
interface TagsOptions extends Omit<CommonOptions<string[]>, "unique"> {
|
|
77
|
+
/** Fewest / most tags. A required field needs at least one. */
|
|
78
|
+
minItems?: number;
|
|
79
|
+
maxItems?: number;
|
|
80
|
+
/** Longest a single tag may be. Default 32. */
|
|
81
|
+
maxLength?: number;
|
|
82
|
+
/** Shown in the input before anything is typed, e.g. "add a label". */
|
|
83
|
+
placeholder?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Anything that is not table-shaped: a settings blob, a webhook payload, metadata.
|
|
87
|
+
*
|
|
88
|
+
* Stored as JSON — a `text` column on SQLite, `jsonb` on Postgres. Validated as parseable
|
|
89
|
+
* and nothing more, because the point of the field is that its shape is not declared. If
|
|
90
|
+
* you find yourself reaching for it to hold something with a known shape, that shape wants
|
|
91
|
+
* fields of its own, or a resource of its own.
|
|
92
|
+
*/
|
|
93
|
+
interface JsonOptions extends Omit<CommonOptions<unknown>, "unique"> {
|
|
94
|
+
/** Reject anything that is not a JSON object, for a settings blob. Default false. */
|
|
95
|
+
object?: boolean;
|
|
56
96
|
}
|
|
57
97
|
/**
|
|
58
98
|
* What a number means, which decides how it is shown and entered.
|
|
@@ -121,6 +161,8 @@ type EnumField<O extends string = string, R extends boolean = boolean> = Base<"e
|
|
|
121
161
|
type MultiSelectField<O extends string = string, R extends boolean = boolean> = Base<"multiselect", R> & MultiSelectOptions<O> & {
|
|
122
162
|
options: readonly O[];
|
|
123
163
|
};
|
|
164
|
+
type TagsField<R extends boolean = boolean> = Base<"tags", R> & TagsOptions;
|
|
165
|
+
type JsonField<R extends boolean = boolean> = Base<"json", R> & JsonOptions;
|
|
124
166
|
type FileField<R extends boolean = boolean> = Base<"file", R> & FileOptions & {
|
|
125
167
|
/** Accepted categories from the `file:[image,pdf]` grammar. */
|
|
126
168
|
accept: readonly FileCategory[];
|
|
@@ -134,7 +176,7 @@ type HasManyField = {
|
|
|
134
176
|
required: false;
|
|
135
177
|
target: string;
|
|
136
178
|
} & HasManyOptions;
|
|
137
|
-
type Field = StringField | TextField | IntField | FloatField | BooleanField | DateField | DateTimeField | EnumField | MultiSelectField | FileField | BelongsToField | HasManyField;
|
|
179
|
+
type Field = StringField | TextField | IntField | FloatField | BooleanField | DateField | DateTimeField | EnumField | MultiSelectField | TagsField | JsonField | FileField | BelongsToField | HasManyField;
|
|
138
180
|
type StoredField = Exclude<Field, HasManyField>;
|
|
139
181
|
/** File categories usable in `file:[...]` and the MIME types each allows. */
|
|
140
182
|
declare const FILE_CATEGORIES: {
|
|
@@ -172,6 +214,12 @@ declare const field: {
|
|
|
172
214
|
radio: <const V extends readonly [string, ...string[]], const O extends EnumOptions<V[number]>>(values: V, options?: O) => EnumField<V[number], Req<O>> & O;
|
|
173
215
|
/** Any number of a list, picked with checkboxes. Stored as a JSON array. */
|
|
174
216
|
multiselect: <const V extends readonly [string, ...string[]], const O extends MultiSelectOptions<V[number]>>(values: V, options?: O) => MultiSelectField<V[number], Req<O>> & O;
|
|
217
|
+
/** Free-text labels, as many as wanted. Stored as a JSON array. */
|
|
218
|
+
tags: <const O extends TagsOptions>(options?: O) => TagsField<Req<O>> & O;
|
|
219
|
+
/** Anything not table-shaped: a settings blob, a payload, metadata. Stored as JSON. */
|
|
220
|
+
json: <const O extends JsonOptions>(options?: O) => JsonField<Req<O>> & O;
|
|
221
|
+
/** Long text written as Markdown: a split editor, rendered where it is shown. */
|
|
222
|
+
markdown: <const O extends Omit<TextOptions, "format">>(options?: O) => TextField<Req<O>> & O;
|
|
175
223
|
email: <const O extends Omit<StringOptions, "format">>(options?: O) => StringField<Req<O>> & O;
|
|
176
224
|
url: <const O extends Omit<StringOptions, "format">>(options?: O) => StringField<Req<O>> & O;
|
|
177
225
|
tel: <const O extends Omit<StringOptions, "format">>(options?: O) => StringField<Req<O>> & O;
|
|
@@ -232,7 +280,7 @@ type RecordOf<Fields> = Simplify<{
|
|
|
232
280
|
} ? FieldValue<Fields[K]> : FieldValue<Fields[K]> | null;
|
|
233
281
|
}>;
|
|
234
282
|
|
|
235
|
-
declare const RESERVED_FIELD_NAMES: readonly ["id", "createdAt", "updatedAt"];
|
|
283
|
+
declare const RESERVED_FIELD_NAMES: readonly ["id", "createdAt", "updatedAt", "deletedAt"];
|
|
236
284
|
type FieldKey<Fields> = Extract<keyof Fields, string>;
|
|
237
285
|
interface ResourceConfig<Fields extends Record<string, Field>> {
|
|
238
286
|
/** PascalCase singular name, e.g. "Contact", "OrderItem". */
|
|
@@ -259,6 +307,20 @@ interface ResourceConfig<Fields extends Record<string, Field>> {
|
|
|
259
307
|
};
|
|
260
308
|
/** Page size for list endpoints and tables. Default 25. */
|
|
261
309
|
perPage?: number;
|
|
310
|
+
/**
|
|
311
|
+
* Keep deleted records instead of removing them.
|
|
312
|
+
*
|
|
313
|
+
* Adds a nullable `deletedAt` column. A delete stamps it; every read leaves those rows
|
|
314
|
+
* out, so nothing else in the app has to know. The dashboard grows a Trash view with
|
|
315
|
+
* Restore and Delete forever, and `?deleted=only` lists them over the API.
|
|
316
|
+
*
|
|
317
|
+
* Two things it does not change, both worth knowing before switching it on. A unique
|
|
318
|
+
* value is still taken by a deleted row — an email freed by deleting a user cannot be
|
|
319
|
+
* signed up again until the row is really gone. And a foreign key still points at it, so
|
|
320
|
+
* a deleted parent keeps its children valid, which is usually what you wanted but is a
|
|
321
|
+
* decision rather than an accident.
|
|
322
|
+
*/
|
|
323
|
+
softDelete?: boolean;
|
|
262
324
|
/**
|
|
263
325
|
* Your logic, beside the fields it belongs to. Every write — the REST API, the
|
|
264
326
|
* dashboard's forms, a seed, an import — goes through the same store, so a hook here
|
|
@@ -323,6 +385,8 @@ interface Resource<Fields extends Record<string, Field> = Record<string, Field>>
|
|
|
323
385
|
direction: "asc" | "desc";
|
|
324
386
|
};
|
|
325
387
|
perPage: number;
|
|
388
|
+
/** Deleted records are kept and hidden, not removed. See ResourceConfig.softDelete. */
|
|
389
|
+
softDelete: boolean;
|
|
326
390
|
/** Fields with labels filled in. */
|
|
327
391
|
fields: Fields & Record<string, Field & {
|
|
328
392
|
label: string;
|
|
@@ -364,13 +428,33 @@ declare function storedFields(resource: Pick<Resource, "fields">): [string, Stor
|
|
|
364
428
|
declare const columnName: (key: string) => string;
|
|
365
429
|
|
|
366
430
|
/**
|
|
367
|
-
* Resource-level policies: which roles may read, create, update, or delete a resource
|
|
431
|
+
* Resource-level policies: which roles may read, create, update, or delete a resource,
|
|
432
|
+
* and — with `own` — which rows each user may touch.
|
|
368
433
|
*
|
|
369
434
|
* Policies are plain data, like descriptors, so the API layer and the admin UI read the
|
|
370
|
-
* same rules at runtime.
|
|
371
|
-
*
|
|
435
|
+
* same rules at runtime. That is why ownership is a field name and a list of exempt
|
|
436
|
+
* roles rather than a callback: it can be read, printed and reasoned about without being
|
|
437
|
+
* run, and it crosses to the dashboard unchanged. Field-level rules are still out of
|
|
438
|
+
* scope.
|
|
439
|
+
*
|
|
440
|
+
* Roles and ownership answer different questions and compose. The role decides whether
|
|
441
|
+
* you may list invoices at all; ownership decides which invoices "all of them" means.
|
|
372
442
|
*/
|
|
373
443
|
type PolicyAction = "list" | "read" | "create" | "update" | "delete";
|
|
444
|
+
/**
|
|
445
|
+
* Per-record ownership: each user sees and changes only the rows that are theirs.
|
|
446
|
+
*
|
|
447
|
+
* `field` is a field on the resource holding the owning user's id — normally a
|
|
448
|
+
* `belongsTo("User")`. The store fills it in on create, requires it to match on read,
|
|
449
|
+
* update and delete, and adds it to every list query, so a row that isn't yours is a
|
|
450
|
+
* 404 rather than a 403: a user who can't see a record shouldn't learn it exists.
|
|
451
|
+
*/
|
|
452
|
+
interface PolicyOwnership {
|
|
453
|
+
/** The field holding the owning user's id, e.g. "userId". */
|
|
454
|
+
field: string;
|
|
455
|
+
/** Roles that see and change every row regardless — usually `["admin"]`. */
|
|
456
|
+
except: string[];
|
|
457
|
+
}
|
|
374
458
|
/** Roles allowed per action; `"*"` means any signed-in user. */
|
|
375
459
|
interface Policy {
|
|
376
460
|
resource: string;
|
|
@@ -378,6 +462,8 @@ interface Policy {
|
|
|
378
462
|
create: string[];
|
|
379
463
|
update: string[];
|
|
380
464
|
delete: string[];
|
|
465
|
+
/** Restrict every action to the user's own rows. Absent means no restriction. */
|
|
466
|
+
own?: PolicyOwnership;
|
|
381
467
|
}
|
|
382
468
|
interface PolicyConfig {
|
|
383
469
|
resource: string;
|
|
@@ -385,6 +471,16 @@ interface PolicyConfig {
|
|
|
385
471
|
create?: string[];
|
|
386
472
|
update?: string[];
|
|
387
473
|
delete?: string[];
|
|
474
|
+
/**
|
|
475
|
+
* Restrict every action to the user's own rows.
|
|
476
|
+
*
|
|
477
|
+
* `{ field: "userId" }` is the common case: `except` defaults to none, so even an
|
|
478
|
+
* admin sees only their own unless you say otherwise.
|
|
479
|
+
*/
|
|
480
|
+
own?: {
|
|
481
|
+
field: string;
|
|
482
|
+
except?: string[];
|
|
483
|
+
};
|
|
388
484
|
}
|
|
389
485
|
declare class PolicyError extends Error {
|
|
390
486
|
constructor(message: string);
|
|
@@ -396,7 +492,20 @@ declare function definePolicy(config: PolicyConfig): Policy;
|
|
|
396
492
|
* `flare gen policy <Resource> --roles …` to restrict a resource.
|
|
397
493
|
*/
|
|
398
494
|
declare function can(policy: Policy | undefined | null, role: string | null | undefined, action: PolicyAction): boolean;
|
|
495
|
+
/**
|
|
496
|
+
* The rows this user is limited to, or null when they may touch every row.
|
|
497
|
+
*
|
|
498
|
+
* Null for three different reasons — no policy, no `own`, or a role in `except` — and
|
|
499
|
+
* the caller treats them the same, because they mean the same thing.
|
|
500
|
+
*/
|
|
501
|
+
declare function ownershipFilter(policy: Policy | undefined | null, user: {
|
|
502
|
+
id: string;
|
|
503
|
+
role?: string | null;
|
|
504
|
+
} | null | undefined): {
|
|
505
|
+
field: string;
|
|
506
|
+
value: string;
|
|
507
|
+
} | null;
|
|
399
508
|
/** Actions `role` may perform, for hiding UI it can't use. */
|
|
400
509
|
declare function allowedActions(policy: Policy | undefined | null, role: string | null | undefined): Record<Exclude<PolicyAction, "list">, boolean>;
|
|
401
510
|
|
|
402
|
-
export {
|
|
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 };
|
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-
|
|
3
|
+
import { S as StoredField, R as Resource, F as Field, P as Policy } from './policy-DFimkxdx.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The handful of things a resource store needs a database to do.
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
columnName,
|
|
4
4
|
createValidators,
|
|
5
5
|
storedFields
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-AAXNSIZG.js";
|
|
7
7
|
|
|
8
8
|
// ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/entity.js
|
|
9
9
|
var entityKind = /* @__PURE__ */ Symbol.for("drizzle:entityKind");
|
|
@@ -1799,6 +1799,20 @@ function fieldJsonSchema(def) {
|
|
|
1799
1799
|
if (def.maxItems !== void 0) schema.maxItems = def.maxItems;
|
|
1800
1800
|
return schema;
|
|
1801
1801
|
}
|
|
1802
|
+
case "tags": {
|
|
1803
|
+
const schema = {
|
|
1804
|
+
...base,
|
|
1805
|
+
type: "array",
|
|
1806
|
+
items: { type: "string", maxLength: def.maxLength ?? 32 },
|
|
1807
|
+
uniqueItems: true,
|
|
1808
|
+
examples: [["urgent", "q3"]]
|
|
1809
|
+
};
|
|
1810
|
+
if (def.minItems !== void 0) schema.minItems = def.minItems;
|
|
1811
|
+
if (def.maxItems !== void 0) schema.maxItems = def.maxItems;
|
|
1812
|
+
return schema;
|
|
1813
|
+
}
|
|
1814
|
+
case "json":
|
|
1815
|
+
return def.object ? { ...base, type: "object", description: base.description ?? "A JSON object." } : { ...base, description: base.description ?? "Any JSON value." };
|
|
1802
1816
|
case "file":
|
|
1803
1817
|
return { ...base, type: "string", description: base.description ?? "Storage key of an uploaded file." };
|
|
1804
1818
|
case "belongsTo":
|