@flaredev/core 0.1.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/client.js ADDED
@@ -0,0 +1,52 @@
1
+ // src/client.ts
2
+ var ApiError = class extends Error {
3
+ status;
4
+ issues;
5
+ field;
6
+ constructor(status, body) {
7
+ super(body?.error ?? `Request failed with status ${status}`);
8
+ this.name = "ApiError";
9
+ this.status = status;
10
+ this.issues = body?.issues ?? [];
11
+ this.field = body?.field;
12
+ }
13
+ };
14
+ function createResourceClient(path, options = {}) {
15
+ const doFetch = options.fetch ?? ((input, init) => fetch(input, init));
16
+ const root = `${options.baseURL?.replace(/\/+$/, "") ?? ""}${path.replace(/\/+$/, "")}`;
17
+ async function request(url, init = {}) {
18
+ const headers = new Headers(options.headers);
19
+ new Headers(init.headers).forEach((value, key) => headers.set(key, value));
20
+ if (init.body !== void 0) headers.set("content-type", "application/json");
21
+ headers.set("accept", "application/json");
22
+ const response = await doFetch(url, { credentials: "same-origin", ...init, headers });
23
+ if (response.status === 204) return void 0;
24
+ const body = await response.json().catch(() => null);
25
+ if (!response.ok) throw new ApiError(response.status, body);
26
+ return body;
27
+ }
28
+ const itemUrl = (id) => `${root}/${encodeURIComponent(id)}`;
29
+ return {
30
+ list(params = {}) {
31
+ const search = new URLSearchParams();
32
+ if (params.page !== void 0) search.set("page", String(params.page));
33
+ if (params.perPage !== void 0) search.set("perPage", String(params.perPage));
34
+ if (params.sort) search.set("sort", params.sort);
35
+ if (params.q) search.set("q", params.q);
36
+ for (const [key, value] of Object.entries(params.filter ?? {})) search.set(`filter[${key}]`, String(value));
37
+ const query = search.toString();
38
+ return request(query ? `${root}?${query}` : root);
39
+ },
40
+ get: (id) => request(itemUrl(id)),
41
+ create: (input) => request(root, { method: "POST", body: JSON.stringify(input) }),
42
+ /** Partial update (PATCH). */
43
+ update: (id, input) => request(itemUrl(id), { method: "PATCH", body: JSON.stringify(input) }),
44
+ /** Full replacement (PUT): omitted optional fields are cleared. */
45
+ replace: (id, input) => request(itemUrl(id), { method: "PUT", body: JSON.stringify(input) }),
46
+ delete: (id) => request(itemUrl(id), { method: "DELETE" })
47
+ };
48
+ }
49
+ export {
50
+ ApiError,
51
+ createResourceClient
52
+ };
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Field builders for resource descriptors.
3
+ *
4
+ * Every builder returns a plain, serializable object. The API layer, admin UI and
5
+ * generator all read these objects at runtime, so changing a label or a validation
6
+ * rule in a descriptor takes effect without re-running the generator. Only
7
+ * storage-level changes (kind, nullability, uniqueness, relations) need a migration.
8
+ */
9
+ type FieldKind = "string" | "text" | "int" | "float" | "boolean" | "date" | "datetime" | "enum" | "file" | "belongsTo" | "hasMany";
10
+ /** Options shared by every stored (non-hasMany) field. */
11
+ interface CommonOptions<T> {
12
+ label?: string;
13
+ /** Required fields must be present on create and can't be null. Default true. */
14
+ required?: boolean;
15
+ unique?: boolean;
16
+ /** Used when the field is omitted on create (also written as the column default). */
17
+ default?: T;
18
+ helpText?: string;
19
+ placeholder?: string;
20
+ /** Show as a column in list views. Default true for most kinds, false for long text. */
21
+ list?: boolean;
22
+ sortable?: boolean;
23
+ filterable?: boolean;
24
+ /** Included in free-text search. Default true for string fields. */
25
+ searchable?: boolean;
26
+ }
27
+ interface StringOptions extends CommonOptions<string> {
28
+ maxLength?: number;
29
+ minLength?: number;
30
+ format?: "email" | "url";
31
+ pattern?: string;
32
+ }
33
+ interface TextOptions extends CommonOptions<string> {
34
+ maxLength?: number;
35
+ minLength?: number;
36
+ }
37
+ interface NumberOptions extends CommonOptions<number> {
38
+ min?: number;
39
+ max?: number;
40
+ }
41
+ type BooleanOptions = CommonOptions<boolean>;
42
+ type DateOptions = CommonOptions<string>;
43
+ interface EnumOptions<O extends string> extends CommonOptions<O> {
44
+ /** Display labels per option value. */
45
+ optionLabels?: Partial<Record<O, string>>;
46
+ }
47
+ interface FileOptions extends CommonOptions<string> {
48
+ /** Maximum upload size in bytes. Default 10 MiB. */
49
+ maxBytes?: number;
50
+ }
51
+ interface BelongsToOptions extends Omit<CommonOptions<string>, "default" | "unique"> {
52
+ /** What happens to this record when the parent is deleted. Default "restrict". */
53
+ onDelete?: "cascade" | "restrict" | "set null";
54
+ unique?: boolean;
55
+ }
56
+ interface HasManyOptions {
57
+ label?: string;
58
+ /** The belongsTo field on the related resource pointing back here. Default: camelCase of this resource's name. */
59
+ foreignKey?: string;
60
+ }
61
+ type Req<O> = O extends {
62
+ required: false;
63
+ } ? false : true;
64
+ interface Base<K extends FieldKind, R extends boolean> {
65
+ kind: K;
66
+ required: R;
67
+ }
68
+ type StringField<R extends boolean = boolean> = Base<"string", R> & StringOptions;
69
+ type TextField<R extends boolean = boolean> = Base<"text", R> & TextOptions;
70
+ type IntField<R extends boolean = boolean> = Base<"int", R> & NumberOptions;
71
+ type FloatField<R extends boolean = boolean> = Base<"float", R> & NumberOptions;
72
+ type BooleanField<R extends boolean = boolean> = Base<"boolean", R> & BooleanOptions;
73
+ type DateField<R extends boolean = boolean> = Base<"date", R> & DateOptions;
74
+ type DateTimeField<R extends boolean = boolean> = Base<"datetime", R> & DateOptions;
75
+ type EnumField<O extends string = string, R extends boolean = boolean> = Base<"enum", R> & EnumOptions<O> & {
76
+ options: readonly O[];
77
+ };
78
+ type FileField<R extends boolean = boolean> = Base<"file", R> & FileOptions & {
79
+ /** Accepted categories from the `file:[image,pdf]` grammar. */
80
+ accept: readonly FileCategory[];
81
+ };
82
+ type BelongsToField<R extends boolean = boolean> = Base<"belongsTo", R> & BelongsToOptions & {
83
+ /** PascalCase name of the related resource. */
84
+ target: string;
85
+ };
86
+ type HasManyField = {
87
+ kind: "hasMany";
88
+ required: false;
89
+ target: string;
90
+ } & HasManyOptions;
91
+ type Field = StringField | TextField | IntField | FloatField | BooleanField | DateField | DateTimeField | EnumField | FileField | BelongsToField | HasManyField;
92
+ type StoredField = Exclude<Field, HasManyField>;
93
+ /** File categories usable in `file:[...]` and the MIME types each allows. */
94
+ declare const FILE_CATEGORIES: {
95
+ readonly image: readonly ["image/png", "image/jpeg", "image/gif", "image/webp", "image/avif"];
96
+ readonly pdf: readonly ["application/pdf"];
97
+ readonly video: readonly ["video/*"];
98
+ readonly audio: readonly ["audio/*"];
99
+ readonly text: readonly ["text/plain"];
100
+ readonly csv: readonly ["text/csv"];
101
+ readonly document: readonly ["application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.oasis.opendocument.text"];
102
+ readonly spreadsheet: readonly ["application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.oasis.opendocument.spreadsheet"];
103
+ readonly archive: readonly ["application/zip"];
104
+ };
105
+ type FileCategory = keyof typeof FILE_CATEGORIES;
106
+ declare function mimeTypesFor(accept: readonly FileCategory[]): string[];
107
+ /** Upload limit for file fields without an explicit `maxBytes` (10 MB). */
108
+ declare const DEFAULT_FILE_MAX_BYTES: number;
109
+ /** The upload limit a file field enforces. */
110
+ declare const fileMaxBytes: (field: {
111
+ maxBytes?: number;
112
+ }) => number;
113
+ declare const field: {
114
+ string: <const O extends StringOptions>(options?: O) => StringField<Req<O>> & O;
115
+ text: <const O extends TextOptions>(options?: O) => TextField<Req<O>> & O;
116
+ int: <const O extends NumberOptions>(options?: O) => IntField<Req<O>> & O;
117
+ float: <const O extends NumberOptions>(options?: O) => FloatField<Req<O>> & O;
118
+ boolean: <const O extends BooleanOptions>(options?: O) => BooleanField<Req<O>> & O;
119
+ date: <const O extends DateOptions>(options?: O) => DateField<Req<O>> & O;
120
+ datetime: <const O extends DateOptions>(options?: O) => DateTimeField<Req<O>> & O;
121
+ enum: <const V extends readonly [string, ...string[]], const O extends EnumOptions<V[number]>>(values: V, options?: O) => EnumField<V[number], Req<O>> & O;
122
+ file: <const O extends FileOptions>(accept: readonly FileCategory[], options?: O) => FileField<Req<O>> & O;
123
+ belongsTo: <const O extends BelongsToOptions>(target: string, options?: O) => BelongsToField<Req<O>> & O;
124
+ hasMany: <const O extends HasManyOptions>(target: string, options?: O) => HasManyField & O;
125
+ };
126
+ type FieldValue<F> = F extends {
127
+ kind: "int" | "float";
128
+ } ? number : F extends {
129
+ kind: "boolean";
130
+ } ? boolean : F extends {
131
+ kind: "enum";
132
+ options: readonly (infer O)[];
133
+ } ? O : F extends {
134
+ kind: "hasMany";
135
+ } ? never : string;
136
+ type StoredKeys<Fields> = {
137
+ [K in keyof Fields]: Fields[K] extends {
138
+ kind: "hasMany";
139
+ } ? never : K;
140
+ }[keyof Fields];
141
+ type RequiredOnCreate<Fields> = {
142
+ [K in StoredKeys<Fields>]: Fields[K] extends {
143
+ required: true;
144
+ } ? (Fields[K] extends {
145
+ default: unknown;
146
+ } ? never : K) : never;
147
+ }[StoredKeys<Fields>];
148
+ type Simplify<T> = {
149
+ [K in keyof T]: T[K];
150
+ } & {};
151
+ /** Body accepted when creating a record. */
152
+ type CreateInput<Fields> = Simplify<{
153
+ [K in RequiredOnCreate<Fields>]: FieldValue<Fields[K]>;
154
+ } & {
155
+ [K in Exclude<StoredKeys<Fields>, RequiredOnCreate<Fields>>]?: Fields[K] extends {
156
+ required: true;
157
+ } ? FieldValue<Fields[K]> : FieldValue<Fields[K]> | null;
158
+ }>;
159
+ /** Body accepted when updating a record (PATCH semantics: every field optional). */
160
+ type UpdateInput<Fields> = Partial<CreateInput<Fields>>;
161
+ /** A record as returned by the JSON API. */
162
+ type RecordOf<Fields> = Simplify<{
163
+ id: string;
164
+ createdAt: string;
165
+ updatedAt: string;
166
+ } & {
167
+ [K in StoredKeys<Fields>]: Fields[K] extends {
168
+ required: true;
169
+ } ? FieldValue<Fields[K]> : FieldValue<Fields[K]> | null;
170
+ }>;
171
+
172
+ declare const RESERVED_FIELD_NAMES: readonly ["id", "createdAt", "updatedAt"];
173
+ type FieldKey<Fields> = Extract<keyof Fields, string>;
174
+ interface ResourceConfig<Fields extends Record<string, Field>> {
175
+ /** PascalCase singular name, e.g. "Contact", "OrderItem". */
176
+ name: string;
177
+ fields: Fields;
178
+ /** Database table. Default: snake_case plural ("order_items"). */
179
+ table?: string;
180
+ /** URL segment for the API and admin. Default: kebab-case plural ("order-items"). */
181
+ slug?: string;
182
+ label?: string;
183
+ pluralLabel?: string;
184
+ /** lucide-react icon name for navigation, e.g. "users". Default: a generic document icon. */
185
+ icon?: string;
186
+ /** Field used to represent a record in lists and relation pickers. Default: first string field. */
187
+ titleField?: FieldKey<Fields>;
188
+ defaultSort?: {
189
+ field: FieldKey<Fields> | "createdAt" | "updatedAt";
190
+ direction: "asc" | "desc";
191
+ };
192
+ /** Page size for list endpoints and tables. Default 25. */
193
+ perPage?: number;
194
+ }
195
+ interface Resource<Fields extends Record<string, Field> = Record<string, Field>> {
196
+ name: string;
197
+ table: string;
198
+ slug: string;
199
+ label: string;
200
+ pluralLabel: string;
201
+ icon: string | undefined;
202
+ titleField: string;
203
+ defaultSort: {
204
+ field: string;
205
+ direction: "asc" | "desc";
206
+ };
207
+ perPage: number;
208
+ /** Fields with labels filled in. */
209
+ fields: Fields & Record<string, Field & {
210
+ label: string;
211
+ }>;
212
+ /** Type-only helpers: `typeof contact.$types.record`. Always undefined at runtime. */
213
+ $types: {
214
+ create: CreateInput<Fields>;
215
+ update: UpdateInput<Fields>;
216
+ record: RecordOf<Fields>;
217
+ };
218
+ }
219
+ declare class ResourceDefinitionError extends Error {
220
+ constructor(resource: string, message: string);
221
+ }
222
+ /** Label for a field key; belongsTo keys drop their "Id" suffix ("companyId" → "Company"). */
223
+ declare function defaultFieldLabel(key: string, field: Field): string;
224
+ declare function defineResource<const Fields extends Record<string, Field>>(config: ResourceConfig<Fields>): Resource<Fields>;
225
+ /** Stored (column-backed) fields of a resource, in declaration order. */
226
+ declare function storedFields(resource: Resource): [string, StoredField & {
227
+ label: string;
228
+ }][];
229
+ /** Database column name for a field key ("companyId" → "company_id"). */
230
+ declare const columnName: (key: string) => string;
231
+
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 };