@flaredev/core 0.4.0 → 0.5.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.
@@ -1,6 +1,9 @@
1
1
  // src/resource/fields.ts
2
2
  var STRING_FORMATS = ["email", "url", "tel", "domain", "country", "color", "slug"];
3
3
  var FILE_CATEGORIES = {
4
+ // For a field that holds whatever someone has: a drive, an attachment, a backup.
5
+ // Nothing is checked beyond the size limit, so don't reach for it out of convenience.
6
+ any: ["*/*"],
4
7
  image: ["image/png", "image/jpeg", "image/gif", "image/webp", "image/avif"],
5
8
  pdf: ["application/pdf"],
6
9
  video: ["video/*"],
@@ -17,7 +20,18 @@ var FILE_CATEGORIES = {
17
20
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
18
21
  "application/vnd.oasis.opendocument.spreadsheet"
19
22
  ],
20
- archive: ["application/zip"]
23
+ // Browsers disagree about what a .zip is — Windows sends x-zip-compressed — and an
24
+ // archive field that refuses an ordinary zip is no use, so the aliases are listed too.
25
+ archive: [
26
+ "application/zip",
27
+ "application/x-zip-compressed",
28
+ "application/x-zip",
29
+ "application/gzip",
30
+ "application/x-gzip",
31
+ "application/x-7z-compressed",
32
+ "application/vnd.rar",
33
+ "application/x-rar-compressed"
34
+ ]
21
35
  };
22
36
  function mimeTypesFor(accept) {
23
37
  return [...new Set(accept.flatMap((category) => FILE_CATEGORIES[category]))];
@@ -87,6 +101,13 @@ function pluralize(value) {
87
101
 
88
102
  // src/resource/define.ts
89
103
  var RESERVED_FIELD_NAMES = ["id", "createdAt", "updatedAt"];
104
+ function clientResource(resource) {
105
+ const { hooks, computed, $types, ...rest } = resource;
106
+ void hooks;
107
+ void computed;
108
+ void $types;
109
+ return rest;
110
+ }
90
111
  var ResourceDefinitionError = class extends Error {
91
112
  constructor(resource, message) {
92
113
  super(`Resource "${resource}": ${message}`);
@@ -385,6 +406,7 @@ export {
385
406
  humanize,
386
407
  pluralize,
387
408
  RESERVED_FIELD_NAMES,
409
+ clientResource,
388
410
  ResourceDefinitionError,
389
411
  defaultFieldLabel,
390
412
  defineResource,
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-CvfE-wCK.js';
2
- export { B as BelongsToField, a as BooleanField, D as DEFAULT_FILE_MAX_BYTES, b as DateField, c as DateTimeField, E as EnumField, d as FILE_CATEGORIES, e as FieldKind, f as FieldValue, g as FileCategory, h as FileField, i as FloatField, H as HasManyField, I as IntField, M as MultiSelectField, P as Policy, j as PolicyAction, k as PolicyConfig, l as PolicyError, m as RESERVED_FIELD_NAMES, n as RecordOf, o as ResourceConfig, p as ResourceDefinitionError, q as STRING_FORMATS, r as StringField, s as StringFormat, T as TextField, t as allowedActions, u as can, v as columnName, w as defaultFieldLabel, x as definePolicy, y as defineResource, z as field, A as fileMaxBytes, G as mimeTypesFor, J as storedFields } from './policy-CvfE-wCK.js';
1
+ import { F as Field, C as CreateInput, U as UpdateInput, R as Resource, S as StoredField } from './policy-DaKcWZ5g.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, M as MultiSelectField, P as Policy, k as PolicyAction, l as PolicyConfig, m as PolicyError, n as RESERVED_FIELD_NAMES, o as RecordOf, p as ResourceConfig, q as ResourceDefinitionError, r as STRING_FORMATS, s as StringField, t as StringFormat, T as TextField, u as allowedActions, v as can, w as clientResource, x as columnName, y as defaultFieldLabel, z as definePolicy, A as defineResource, G as field, J as fileMaxBytes, K as mimeTypesFor, L as storedFields } from './policy-DaKcWZ5g.js';
3
3
  import { z } from 'zod';
4
4
 
5
5
  /**
@@ -125,6 +125,22 @@ interface StorageBucket {
125
125
  };
126
126
  }): Promise<unknown>;
127
127
  delete(keys: string | string[]): Promise<void>;
128
+ /**
129
+ * The keys in the bucket, a page at a time. R2 counts this as a Class A operation —
130
+ * the expensive kind — so call it to answer a question, not on a page people reload.
131
+ */
132
+ list?(options?: {
133
+ limit?: number;
134
+ prefix?: string;
135
+ cursor?: string;
136
+ }): Promise<{
137
+ objects: {
138
+ key: string;
139
+ size: number;
140
+ }[];
141
+ truncated: boolean;
142
+ cursor?: string;
143
+ }>;
128
144
  }
129
145
  interface StorageObjectBody {
130
146
  body: ReadableStream;
@@ -286,7 +302,7 @@ declare function fieldSchema(def: StoredField): z.ZodType;
286
302
  * edit a record could attach another resource's private file and read it through theirs.
287
303
  */
288
304
  declare const fileKeyPrefix: (resource: Pick<Resource, "table">, fieldKey: string) => string;
289
- declare function createValidators<Fields extends Record<string, Field>>(resource: Resource<Fields>): ResourceValidators<Fields>;
305
+ declare function createValidators<Fields extends Record<string, Field>>(resource: Pick<Resource<Fields>, "fields" | "table">): ResourceValidators<Fields>;
290
306
 
291
307
  /** Naming helpers shared by descriptors and the generator. */
292
308
  /** "OrderItem" / "order item" / "order-item" → ["order", "item"] */
@@ -365,13 +381,13 @@ declare function formatValue(def: StoredField | {
365
381
  */
366
382
  type FormValues = Record<string, string | boolean>;
367
383
  /** Initial form state: the record's values (edit) or the descriptor's defaults (create). */
368
- declare function initialFormValues(resource: Resource, record?: Record<string, unknown> | null): FormValues;
384
+ declare function initialFormValues(resource: Pick<Resource, "fields">, record?: Record<string, unknown> | null): FormValues;
369
385
  /**
370
386
  * API input from form state. Empty optional fields become null (clearing them);
371
387
  * empty fields that have a default are omitted on create so the default applies;
372
388
  * empty required fields are left for validation to report as "Required".
373
389
  */
374
- declare function formValuesToInput(resource: Resource, values: FormValues, mode: "create" | "edit"): Record<string, unknown>;
390
+ declare function formValuesToInput(resource: Pick<Resource, "fields">, values: FormValues, mode: "create" | "edit"): Record<string, unknown>;
375
391
  /** A multiselect's form state (a JSON array string) as the picked values. */
376
392
  declare function parseMultiValue(raw: string | boolean | undefined): string[];
377
393
  /** Field errors keyed by field name, from validation or API issues (first message wins). */
@@ -439,6 +455,8 @@ interface Fake {
439
455
  email(name?: string): string;
440
456
  company(): string;
441
457
  jobTitle(): string;
458
+ /** "Brushed steel desk lamp", or a digital one: "Starter kit licence". */
459
+ product(digital?: boolean): string;
442
460
  city(): string;
443
461
  /** ISO 3166-1 alpha-2, e.g. "UG". */
444
462
  country(): string;
@@ -495,7 +513,11 @@ declare const fake: Fake;
495
513
  */
496
514
  type InsertMany = <T extends object>(table: unknown, count: number | readonly T[], build?: (index: number) => T) => Promise<number>;
497
515
  interface SeedContext {
498
- /** Drizzle client over the app's schema, bound to the local D1 database. */
516
+ /**
517
+ * The app's database client: Drizzle over the local D1 database on Cloudflare, the
518
+ * app's own Prisma client on Next.js. Untyped, because which one it is depends on
519
+ * the stack and both are generated per app.
520
+ */
499
521
  db: any;
500
522
  /** All local bindings and variables (D1, R2, .dev.vars). */
501
523
  env: Record<string, unknown>;
@@ -507,6 +529,6 @@ interface SeedContext {
507
529
  type Seed = (context: SeedContext) => void | Promise<void>;
508
530
  declare function defineSeed(seed: Seed): Seed;
509
531
 
510
- declare const FLARE_VERSION = "0.4.0";
532
+ declare const FLARE_VERSION = "0.5.0";
511
533
 
512
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
@@ -8,6 +8,7 @@ import {
8
8
  allowedActions,
9
9
  camelCase,
10
10
  can,
11
+ clientResource,
11
12
  columnName,
12
13
  countries,
13
14
  countryName,
@@ -38,7 +39,7 @@ import {
38
39
  storedFields,
39
40
  toE164,
40
41
  words
41
- } from "./chunk-PWP4QEZV.js";
42
+ } from "./chunk-VLHPSNQN.js";
42
43
 
43
44
  // src/password.ts
44
45
  var ALGORITHM = "pbkdf2-sha256";
@@ -241,6 +242,8 @@ var EMPTY_ZIP = [80, 75, 5, 6];
241
242
  var OLE2 = [208, 207, 17, 224, 161, 177, 26, 225];
242
243
  var EBML = [26, 69, 223, 163];
243
244
  var isZip = (head) => startsWith(head, ZIP) || startsWith(head, EMPTY_ZIP);
245
+ var isGzip = (head) => startsWith(head, [31, 139]);
246
+ var isRar = (head) => startsWith(head, ascii("Rar!"));
244
247
  var EXACT = {
245
248
  "image/png": (head) => startsWith(head, [137, 80, 78, 71, 13, 10, 26, 10]),
246
249
  "image/jpeg": (head) => startsWith(head, [255, 216, 255]),
@@ -249,6 +252,13 @@ var EXACT = {
249
252
  "image/avif": (head) => ["avif", "avis", "mif1", "msf1"].includes(isoBrand(head) ?? ""),
250
253
  "application/pdf": (head) => startsWith(head, ascii("%PDF-")),
251
254
  "application/zip": isZip,
255
+ "application/x-zip-compressed": isZip,
256
+ "application/x-zip": isZip,
257
+ "application/gzip": isGzip,
258
+ "application/x-gzip": isGzip,
259
+ "application/x-7z-compressed": (head) => startsWith(head, [55, 122, 188, 175, 39, 28]),
260
+ "application/vnd.rar": isRar,
261
+ "application/x-rar-compressed": isRar,
252
262
  // Office Open XML and OpenDocument files are zip archives.
253
263
  "application/vnd.openxmlformats-officedocument.wordprocessingml.document": isZip,
254
264
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": isZip,
@@ -281,6 +291,7 @@ function matchesContentType(contentType, allowed) {
281
291
  if (!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(type)) return false;
282
292
  return allowed.some((pattern) => {
283
293
  const p = pattern.trim().toLowerCase();
294
+ if (p === "*/*") return true;
284
295
  return p.endsWith("/*") ? type.startsWith(p.slice(0, -1)) : type === p;
285
296
  });
286
297
  }
@@ -873,6 +884,26 @@ var WORDS = [
873
884
  "variance",
874
885
  "workflow"
875
886
  ];
887
+ var PRODUCT_QUALITIES = ["Oak", "Walnut", "Brushed steel", "Canvas", "Merino", "Recycled", "Matte black", "Linen", "Copper", "Frosted glass"];
888
+ var PRODUCT_THINGS = [
889
+ "desk lamp",
890
+ "office chair",
891
+ "notebook",
892
+ "water bottle",
893
+ "backpack",
894
+ "keyboard",
895
+ "mouse mat",
896
+ "coffee grinder",
897
+ "travel mug",
898
+ "wall clock",
899
+ "storage box",
900
+ "cable tidy",
901
+ "monitor stand",
902
+ "planter",
903
+ "door mat",
904
+ "tote bag"
905
+ ];
906
+ var DIGITAL_THINGS = ["starter kit", "source code licence", "icon pack", "template bundle", "course", "font family", "preset pack", "e-book"];
876
907
  var DOMAINS = ["example.com", "example.org", "example.net", "mail.example.com"];
877
908
  var TLDS = ["com", "io", "co", "org", "net"];
878
909
  var COLORS = ["#f2541d", "#2563eb", "#16a34a", "#d97706", "#7c3aed", "#0891b2", "#db2777", "#4b5563"];
@@ -924,6 +955,7 @@ function createFake(seed = 20260101) {
924
955
  return `${slugify2(local).replace(/-/g, ".")}${step()}@${pick(DOMAINS)}`;
925
956
  },
926
957
  company: () => `${pick(COMPANY_HEADS)} ${pick(COMPANY_TAILS)}`,
958
+ product: (digital = false) => digital ? `${pick(PRODUCT_QUALITIES)} ${pick(DIGITAL_THINGS)}` : `${pick(PRODUCT_QUALITIES)} ${pick(PRODUCT_THINGS)}`,
927
959
  jobTitle: () => `${pick(["Head of", "Senior", "Lead", "Regional", "Junior"])} ${pick(["Operations", "Sales", "Support", "Engineering", "Finance", "Logistics"])}`,
928
960
  city: () => pick(CITIES),
929
961
  country: () => pick(COUNTRIES)[0],
@@ -962,7 +994,7 @@ function createFake(seed = 20260101) {
962
994
  var fake = createFake();
963
995
 
964
996
  // src/index.ts
965
- var FLARE_VERSION = "0.4.0";
997
+ var FLARE_VERSION = "0.5.0";
966
998
  export {
967
999
  DEFAULT_FILE_MAX_BYTES,
968
1000
  FILE_CATEGORIES,
@@ -979,6 +1011,7 @@ export {
979
1011
  camelCase,
980
1012
  can,
981
1013
  checkoutPaid,
1014
+ clientResource,
982
1015
  columnName,
983
1016
  countries,
984
1017
  countryName,
@@ -116,6 +116,7 @@ type Field = StringField | TextField | IntField | FloatField | BooleanField | Da
116
116
  type StoredField = Exclude<Field, HasManyField>;
117
117
  /** File categories usable in `file:[...]` and the MIME types each allows. */
118
118
  declare const FILE_CATEGORIES: {
119
+ readonly any: readonly ["*/*"];
119
120
  readonly image: readonly ["image/png", "image/jpeg", "image/gif", "image/webp", "image/avif"];
120
121
  readonly pdf: readonly ["application/pdf"];
121
122
  readonly video: readonly ["video/*"];
@@ -124,7 +125,7 @@ declare const FILE_CATEGORIES: {
124
125
  readonly csv: readonly ["text/csv"];
125
126
  readonly document: readonly ["application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.oasis.opendocument.text"];
126
127
  readonly spreadsheet: readonly ["application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.oasis.opendocument.spreadsheet"];
127
- readonly archive: readonly ["application/zip"];
128
+ readonly archive: readonly ["application/zip", "application/x-zip-compressed", "application/x-zip", "application/gzip", "application/x-gzip", "application/x-7z-compressed", "application/vnd.rar", "application/x-rar-compressed"];
128
129
  };
129
130
  type FileCategory = keyof typeof FILE_CATEGORIES;
130
131
  declare function mimeTypesFor(accept: readonly FileCategory[]): string[];
@@ -261,7 +262,11 @@ interface HookContext {
261
262
  } | null;
262
263
  }
263
264
  interface ResourceHooks {
264
- /** Change or check the input before it's written. Return the input (changed or not), or throw to refuse. */
265
+ /**
266
+ * Change or check the input before it's written. Return the input (changed or not), or
267
+ * throw to refuse. It runs *before* validation, so it can fill in a field the caller
268
+ * couldn't supply — an order number, a slug — and what it returns is then validated.
269
+ */
265
270
  beforeCreate?: (input: Record<string, unknown>, context: HookContext) => Promise<Record<string, unknown>> | Record<string, unknown>;
266
271
  afterCreate?: (record: Record<string, unknown>, context: HookContext) => Promise<void> | void;
267
272
  beforeUpdate?: (input: Record<string, unknown>, context: HookContext & {
@@ -307,6 +312,22 @@ interface Resource<Fields extends Record<string, Field> = Record<string, Field>>
307
312
  record: RecordOf<Fields>;
308
313
  };
309
314
  }
315
+ /**
316
+ * A resource as a client component can receive it: everything that describes the fields,
317
+ * without the code.
318
+ *
319
+ * `hooks` and `computed` hold functions, and a function can't cross from the server to
320
+ * the browser — React refuses a prop it wasn't told to expose, and the page fails to
321
+ * render. So a server component hands a client one `clientResource(resource)` instead.
322
+ * The `never` types below turn passing the whole resource into a type error, which is
323
+ * cheaper to find than the runtime one.
324
+ */
325
+ type ClientResource<Fields extends Record<string, Field> = Record<string, Field>> = Omit<Resource<Fields>, "hooks" | "computed" | "$types"> & {
326
+ hooks?: never;
327
+ computed?: never;
328
+ };
329
+ /** The parts of a resource that are safe to send to the browser. */
330
+ declare function clientResource<Fields extends Record<string, Field>>(resource: Resource<Fields>): ClientResource<Fields>;
310
331
  declare class ResourceDefinitionError extends Error {
311
332
  constructor(resource: string, message: string);
312
333
  }
@@ -314,7 +335,7 @@ declare class ResourceDefinitionError extends Error {
314
335
  declare function defaultFieldLabel(key: string, field: Field): string;
315
336
  declare function defineResource<const Fields extends Record<string, Field>>(config: ResourceConfig<Fields>): Resource<Fields>;
316
337
  /** Stored (column-backed) fields of a resource, in declaration order. */
317
- declare function storedFields(resource: Resource): [string, StoredField & {
338
+ declare function storedFields(resource: Pick<Resource, "fields">): [string, StoredField & {
318
339
  label: string;
319
340
  }][];
320
341
  /** Database column name for a field key ("companyId" → "company_id"). */
@@ -356,4 +377,4 @@ declare function can(policy: Policy | undefined | null, role: string | null | un
356
377
  /** Actions `role` may perform, for hiding UI it can't use. */
357
378
  declare function allowedActions(policy: Policy | undefined | null, role: string | null | undefined): Record<Exclude<PolicyAction, "list">, boolean>;
358
379
 
359
- 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 };
380
+ export { defineResource as A, type BelongsToField as B, type CreateInput as C, DEFAULT_FILE_MAX_BYTES as D, type EnumField as E, type Field as F, field as G, type HasManyField as H, type IntField as I, fileMaxBytes as J, mimeTypesFor as K, storedFields as L, 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 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 PolicyAction as k, type PolicyConfig as l, PolicyError as m, RESERVED_FIELD_NAMES as n, type RecordOf as o, type ResourceConfig as p, ResourceDefinitionError as q, STRING_FORMATS as r, type StringField as s, type StringFormat as t, allowedActions as u, can as v, clientResource as w, columnName as x, defaultFieldLabel as y, definePolicy as z };
package/dist/server.d.ts CHANGED
@@ -1,6 +1,82 @@
1
1
  import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
2
- import { BaseSQLiteDatabase, SQLiteTable } from 'drizzle-orm/sqlite-core';
3
- import { S as StoredField, R as Resource, F as Field, P as Policy } from './policy-CvfE-wCK.js';
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-DaKcWZ5g.js';
4
+
5
+ /**
6
+ * The handful of things a resource store needs a database to do.
7
+ *
8
+ * Everything that makes a Flare resource behave the way it does — validation, hooks,
9
+ * computed values, cursors, capped counts, the wording of a constraint error — lives in
10
+ * the store and is the same on every stack. What differs is how rows are actually read
11
+ * and written, and that is this interface: nine operations, described in the
12
+ * descriptor's terms rather than in SQL.
13
+ *
14
+ * Drizzle over SQLite is one implementation; Prisma over Postgres is another. A store
15
+ * built on either behaves identically, which is the whole point — the same descriptor
16
+ * has to mean the same thing on Cloudflare and on Vercel.
17
+ */
18
+ type Row = Record<string, unknown>;
19
+ /** What a list needs, before it becomes SQL. */
20
+ interface RowsQuery {
21
+ /** Free-text search across the fields the descriptor marks searchable. */
22
+ search?: {
23
+ term: string;
24
+ fields: string[];
25
+ };
26
+ /** field → value, or null for "is null". */
27
+ filters: Record<string, unknown>;
28
+ sort: {
29
+ field: string;
30
+ direction: "asc" | "desc";
31
+ };
32
+ /**
33
+ * Keyset pagination: everything after (or before) this row, compared on
34
+ * `(sort field, id)` together so rows sharing a sort value are neither skipped nor
35
+ * repeated.
36
+ */
37
+ cursor?: {
38
+ field: string;
39
+ value: unknown;
40
+ id: string;
41
+ greaterThan: boolean;
42
+ };
43
+ limit: number;
44
+ /** Offset paging, for the page-number case. Never combined with a cursor. */
45
+ offset?: number;
46
+ }
47
+ /** A database refusing a write, in terms the store can turn into a message. */
48
+ type ConstraintHit = {
49
+ kind: "unique";
50
+ column?: string;
51
+ } | {
52
+ kind: "foreignKey";
53
+ } | {
54
+ kind: "check";
55
+ };
56
+ interface ResourceRows {
57
+ /** The database handle a descriptor's hooks are given. Drizzle, or a Prisma client. */
58
+ readonly db: unknown;
59
+ find(query: RowsQuery): Promise<Row[]>;
60
+ /** How many rows match, giving up at `limit` so a huge table is never fully counted. */
61
+ countUpTo(query: Pick<RowsQuery, "search" | "filters">, limit: number): Promise<number>;
62
+ byId(id: string): Promise<Row | null>;
63
+ titles(ids: string[], titleField: string): Promise<{
64
+ id: string;
65
+ title: unknown;
66
+ }[]>;
67
+ insert(values: Row): Promise<Row>;
68
+ /** The saved row, or null when there was nothing with that id. */
69
+ update(id: string, values: Row): Promise<Row | null>;
70
+ /** Whether a row was actually removed. */
71
+ remove(id: string): Promise<boolean>;
72
+ /** Recognise a constraint violation, or hand the error back by returning undefined. */
73
+ constraint(error: unknown): ConstraintHit | undefined;
74
+ }
75
+
76
+ type AnyDatabase = BaseSQLiteDatabase<"sync" | "async", any, any>;
77
+ declare function drizzleRows(table: SQLiteTable, getDb: () => AnyDatabase): ResourceRows & {
78
+ columns: Record<string, SQLiteColumn>;
79
+ };
4
80
 
5
81
  interface ListQuery {
6
82
  page: number;
@@ -44,7 +120,6 @@ declare function parseListQuery(resource: Resource, params: URLSearchParams): {
44
120
  issues: QueryIssue[];
45
121
  };
46
122
 
47
- type AnyDatabase = BaseSQLiteDatabase<"sync" | "async", any, any>;
48
123
  type ResourceAction = "list" | "read" | "create" | "update" | "delete";
49
124
  interface FieldIssue {
50
125
  /** Dotted field path; "" for the whole body (e.g. unknown keys). */
@@ -106,8 +181,14 @@ interface ResourceStoreOptions {
106
181
  email: string;
107
182
  role?: string | null;
108
183
  } | null;
109
- table: SQLiteTable;
110
- getDb: () => AnyDatabase;
184
+ /**
185
+ * The Drizzle table and database, for the Cloudflare stack. Another stack passes
186
+ * `rows` instead — a Prisma-backed one, say — and everything else is identical.
187
+ */
188
+ table?: SQLiteTable;
189
+ getDb?: () => AnyDatabase;
190
+ /** Where rows come from, when it isn't Drizzle. */
191
+ rows?: ResourceRows;
111
192
  /**
112
193
  * Called after a write succeeds, for cache invalidation. Runs before the
113
194
  * operation returns, so a caller that revalidates tags can't hand back a
@@ -124,15 +205,15 @@ interface ResourceStoreOptions {
124
205
  */
125
206
  declare function createResourceStore(options: ResourceStoreOptions): {
126
207
  resource: Resource<Record<string, Field>>;
127
- table: SQLiteTable<drizzle_orm_sqlite_core.TableConfig>;
208
+ table: SQLiteTable<drizzle_orm_sqlite_core.TableConfig> | undefined;
128
209
  /** `params`: page, perPage, sort, q, filter[field], cursor (as parsed by parseListQuery). */
129
210
  list(params: URLSearchParams): Promise<Result<ListResult>>;
130
211
  get(id: string): Promise<Result<Record<string, unknown>>>;
131
212
  /** Title-field values for a set of ids (for showing relations). Missing ids are omitted. */
132
213
  titles(ids: string[]): Promise<Record<string, string>>;
133
- create(input: unknown): Promise<Result<Record<string, unknown>>>;
214
+ create(raw: unknown): Promise<Result<Record<string, unknown>>>;
134
215
  /** Partial update (PATCH semantics). */
135
- update(id: string, input: unknown): Promise<Result<Record<string, unknown>>>;
216
+ update(id: string, raw: unknown): Promise<Result<Record<string, unknown>>>;
136
217
  /** Full replacement (PUT semantics): omitted fields go back to their default, or null when optional. */
137
218
  replace(id: string, input: unknown): Promise<Result<Record<string, unknown>>>;
138
219
  delete(id: string): Promise<Result<{
@@ -151,8 +232,11 @@ interface AuthorizeContext {
151
232
  type Authorize = (context: AuthorizeContext) => Response | void | Promise<Response | void>;
152
233
  interface ResourceHandlerOptions {
153
234
  resource: Resource;
154
- table: SQLiteTable;
155
- getDb: () => AnyDatabase;
235
+ /** Drizzle and D1, on the Cloudflare stack. Another stack passes `rows` instead. */
236
+ table?: SQLiteTable;
237
+ getDb?: () => AnyDatabase;
238
+ /** Where rows come from, when it isn't Drizzle — a Prisma-backed adapter, say. */
239
+ rows?: ResourceRows;
156
240
  authorize: Authorize;
157
241
  /** Who is making the request, for the descriptor's hooks. */
158
242
  currentUser?: ResourceStoreOptions["currentUser"];
@@ -178,12 +262,12 @@ declare function createResourceHandlers(options: ResourceHandlerOptions): {
178
262
  };
179
263
  store: {
180
264
  resource: Resource<Record<string, Field>>;
181
- table: SQLiteTable<drizzle_orm_sqlite_core.TableConfig>;
265
+ table: SQLiteTable<drizzle_orm_sqlite_core.TableConfig> | undefined;
182
266
  list(params: URLSearchParams): Promise<Result<ListResult>>;
183
267
  get(id: string): Promise<Result<Record<string, unknown>>>;
184
268
  titles(ids: string[]): Promise<Record<string, string>>;
185
- create(input: unknown): Promise<Result<Record<string, unknown>>>;
186
- update(id: string, input: unknown): Promise<Result<Record<string, unknown>>>;
269
+ create(raw: unknown): Promise<Result<Record<string, unknown>>>;
270
+ update(id: string, raw: unknown): Promise<Result<Record<string, unknown>>>;
187
271
  replace(id: string, input: unknown): Promise<Result<Record<string, unknown>>>;
188
272
  delete(id: string): Promise<Result<{
189
273
  id: string;
@@ -191,6 +275,37 @@ declare function createResourceHandlers(options: ResourceHandlerOptions): {
191
275
  };
192
276
  };
193
277
 
278
+ /**
279
+ * {@link ResourceRows} over Prisma and Postgres — the Next.js stack's half.
280
+ *
281
+ * The same nine operations the Drizzle adapter implements, expressed as Prisma queries.
282
+ * Everything a resource *means* — validation, hooks, computed values, the capped count,
283
+ * the wording of a unique-constraint error — stays in the store and is shared, so a
284
+ * descriptor behaves the same on either stack.
285
+ *
286
+ * Nothing here imports `@prisma/client`: a generated client is app-specific and this
287
+ * package can't depend on one. The delegate is described structurally instead, which is
288
+ * also what makes it easy to test without a database.
289
+ */
290
+
291
+ /**
292
+ * The part of a Prisma model delegate a resource store uses.
293
+ *
294
+ * The arguments are loose on purpose. Prisma generates a precise argument type per
295
+ * model — `Prisma.ProductFindManyArgs` and so on — and describing them here would mean
296
+ * reproducing Prisma's generics for a client this package can't import. The queries are
297
+ * all built in this file, so there is exactly one place to get them right.
298
+ */
299
+ interface PrismaDelegate {
300
+ findMany(args: any): Promise<any[]>;
301
+ findUnique(args: any): Promise<any>;
302
+ count(args: any): Promise<number>;
303
+ create(args: any): Promise<any>;
304
+ update(args: any): Promise<any>;
305
+ delete(args: any): Promise<any>;
306
+ }
307
+ declare function prismaRows(delegate: PrismaDelegate, client: unknown): ResourceRows;
308
+
194
309
  /**
195
310
  * An OpenAPI 3.1 document for an app's generated REST API, built at request time
196
311
  * from the same descriptors and policies the API itself runs on, so it can't drift.
@@ -218,4 +333,4 @@ interface OpenApiOptions {
218
333
  declare function fieldJsonSchema(def: StoredField): Schema;
219
334
  declare function openApiDocument(options: OpenApiOptions): Schema;
220
335
 
221
- 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 };
336
+ export { type AnyDatabase, type Authorize, type AuthorizeContext, type ChangeEvent, type ConstraintHit, type Failure, type FieldIssue, type ListQuery, type ListResult, type OpenApiOptions, type PrismaDelegate, type QueryIssue, type ResourceAction, type ResourceHandlerOptions, type ResourceRows, type ResourceStore, type ResourceStoreOptions, type Result, type Row, type RowsQuery, createResourceHandlers, createResourceStore, drizzleRows, fieldJsonSchema, isFilterable, isSearchable, isSortable, openApiDocument, parseListQuery, prismaRows };
package/dist/server.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  can,
3
+ columnName,
3
4
  createValidators,
4
5
  storedFields
5
- } from "./chunk-PWP4QEZV.js";
6
+ } from "./chunk-VLHPSNQN.js";
6
7
 
7
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/entity.js
8
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/entity.js
8
9
  var entityKind = /* @__PURE__ */ Symbol.for("drizzle:entityKind");
9
10
  function is(value, type) {
10
11
  if (!value || typeof value !== "object") {
@@ -30,7 +31,7 @@ function is(value, type) {
30
31
  return false;
31
32
  }
32
33
 
33
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/column.js
34
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/column.js
34
35
  var Column = class {
35
36
  constructor(table, config) {
36
37
  this.table = table;
@@ -81,7 +82,7 @@ var Column = class {
81
82
  }
82
83
  };
83
84
 
84
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/column-builder.js
85
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/column-builder.js
85
86
  var ColumnBuilder = class {
86
87
  static [entityKind] = "ColumnBuilder";
87
88
  config;
@@ -184,10 +185,10 @@ var ColumnBuilder = class {
184
185
  }
185
186
  };
186
187
 
187
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/table.utils.js
188
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/table.utils.js
188
189
  var TableName = /* @__PURE__ */ Symbol.for("drizzle:Name");
189
190
 
190
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/pg-core/foreign-keys.js
191
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/pg-core/foreign-keys.js
191
192
  var ForeignKeyBuilder = class {
192
193
  static [entityKind] = "PgForeignKeyBuilder";
193
194
  /** @internal */
@@ -244,12 +245,12 @@ var ForeignKey = class {
244
245
  }
245
246
  };
246
247
 
247
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/tracing-utils.js
248
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/tracing-utils.js
248
249
  function iife(fn, ...args) {
249
250
  return fn(...args);
250
251
  }
251
252
 
252
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/pg-core/unique-constraint.js
253
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/pg-core/unique-constraint.js
253
254
  function uniqueKeyName(table, columns) {
254
255
  return `${table[TableName]}_${columns.join("_")}_unique`;
255
256
  }
@@ -299,7 +300,7 @@ var UniqueConstraint = class {
299
300
  }
300
301
  };
301
302
 
302
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/pg-core/utils/array.js
303
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/pg-core/utils/array.js
303
304
  function parsePgArrayValue(arrayString, startFrom, inQuotes) {
304
305
  for (let i = startFrom; i < arrayString.length; i++) {
305
306
  const char = arrayString[i];
@@ -375,7 +376,7 @@ function makePgArray(array) {
375
376
  }).join(",")}}`;
376
377
  }
377
378
 
378
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/pg-core/columns/common.js
379
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/pg-core/columns/common.js
379
380
  var PgColumnBuilder = class extends ColumnBuilder {
380
381
  foreignKeyConfigs = [];
381
382
  static [entityKind] = "PgColumnBuilder";
@@ -559,7 +560,7 @@ var PgArray = class _PgArray extends PgColumn {
559
560
  }
560
561
  };
561
562
 
562
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/pg-core/columns/enum.js
563
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/pg-core/columns/enum.js
563
564
  var PgEnumObjectColumnBuilder = class extends PgColumnBuilder {
564
565
  static [entityKind] = "PgEnumObjectColumnBuilder";
565
566
  constructor(name, enumInstance) {
@@ -617,7 +618,7 @@ var PgEnumColumn = class extends PgColumn {
617
618
  }
618
619
  };
619
620
 
620
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/subquery.js
621
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/subquery.js
621
622
  var Subquery = class {
622
623
  static [entityKind] = "Subquery";
623
624
  constructor(sql2, fields, alias, isWith = false, usedTables = []) {
@@ -638,10 +639,10 @@ var WithSubquery = class extends Subquery {
638
639
  static [entityKind] = "WithSubquery";
639
640
  };
640
641
 
641
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/version.js
642
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/version.js
642
643
  var version = "0.45.2";
643
644
 
644
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/tracing.js
645
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/tracing.js
645
646
  var otel;
646
647
  var rawTracer;
647
648
  var tracer = {
@@ -676,10 +677,10 @@ var tracer = {
676
677
  }
677
678
  };
678
679
 
679
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/view-common.js
680
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/view-common.js
680
681
  var ViewBaseConfig = /* @__PURE__ */ Symbol.for("drizzle:ViewBaseConfig");
681
682
 
682
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/table.js
683
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/table.js
683
684
  var Schema = /* @__PURE__ */ Symbol.for("drizzle:Schema");
684
685
  var Columns = /* @__PURE__ */ Symbol.for("drizzle:Columns");
685
686
  var ExtraConfigColumns = /* @__PURE__ */ Symbol.for("drizzle:ExtraConfigColumns");
@@ -735,7 +736,7 @@ var Table = class {
735
736
  }
736
737
  };
737
738
 
738
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/sql/sql.js
739
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/sql/sql.js
739
740
  var FakePrimitiveParam = class {
740
741
  static [entityKind] = "FakePrimitiveParam";
741
742
  };
@@ -847,13 +848,13 @@ var SQL = class _SQL {
847
848
  };
848
849
  }
849
850
  if (is(chunk, Column)) {
850
- const columnName = casing.getColumnCasing(chunk);
851
+ const columnName2 = casing.getColumnCasing(chunk);
851
852
  if (_config.invokeSource === "indexes") {
852
- return { sql: escapeName(columnName), params: [] };
853
+ return { sql: escapeName(columnName2), params: [] };
853
854
  }
854
855
  const schemaName = chunk.table[Table.Symbol.Schema];
855
856
  return {
856
- sql: chunk.table[IsAlias] || schemaName === void 0 ? escapeName(chunk.table[Table.Symbol.Name]) + "." + escapeName(columnName) : escapeName(schemaName) + "." + escapeName(chunk.table[Table.Symbol.Name]) + "." + escapeName(columnName),
857
+ sql: chunk.table[IsAlias] || schemaName === void 0 ? escapeName(chunk.table[Table.Symbol.Name]) + "." + escapeName(columnName2) : escapeName(schemaName) + "." + escapeName(chunk.table[Table.Symbol.Name]) + "." + escapeName(columnName2),
857
858
  params: []
858
859
  };
859
860
  }
@@ -1112,13 +1113,13 @@ Subquery.prototype.getSQL = function() {
1112
1113
  return new SQL([this]);
1113
1114
  };
1114
1115
 
1115
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/utils.js
1116
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/utils.js
1116
1117
  function getTableColumns(table) {
1117
1118
  return table[Table.Symbol.Columns];
1118
1119
  }
1119
1120
  var textDecoder = typeof TextDecoder === "undefined" ? null : new TextDecoder();
1120
1121
 
1121
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/sql/expressions/conditions.js
1122
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/sql/expressions/conditions.js
1122
1123
  function bindIfParam(value, column) {
1123
1124
  if (isDriverValueEncoder(column) && !isSQLWrapper(value) && !is(value, Param) && !is(value, Placeholder) && !is(value, Column) && !is(value, Table) && !is(value, View)) {
1124
1125
  return new Param(value, column);
@@ -1173,7 +1174,7 @@ function isNull(value) {
1173
1174
  return sql`${value} is null`;
1174
1175
  }
1175
1176
 
1176
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/sql/expressions/select.js
1177
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/sql/expressions/select.js
1177
1178
  function asc(column) {
1178
1179
  return sql`${column} asc`;
1179
1180
  }
@@ -1181,11 +1182,101 @@ function desc(column) {
1181
1182
  return sql`${column} desc`;
1182
1183
  }
1183
1184
 
1184
- // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_b372b29e6d5511672c7f9f9d2ac40572/node_modules/drizzle-orm/sql/functions/aggregate.js
1185
+ // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/sql/functions/aggregate.js
1185
1186
  function count(expression) {
1186
1187
  return sql`count(${expression || sql.raw("*")})`.mapWith(Number);
1187
1188
  }
1188
1189
 
1190
+ // src/server/drizzle-rows.ts
1191
+ function errorText(error) {
1192
+ const parts = [];
1193
+ for (let current = error, depth = 0; current && depth < 5; depth++) {
1194
+ if (current instanceof Error) {
1195
+ parts.push(current.message);
1196
+ current = current.cause;
1197
+ } else {
1198
+ parts.push(String(current));
1199
+ break;
1200
+ }
1201
+ }
1202
+ return parts.join(" | ");
1203
+ }
1204
+ function drizzleRows(table, getDb) {
1205
+ const columns = getTableColumns(table);
1206
+ const idColumn = columns.id;
1207
+ const byId = (id) => eq(idColumn, id);
1208
+ function matching(query) {
1209
+ const conditions = [];
1210
+ if (query.search && query.search.fields.length > 0) {
1211
+ const pattern = `%${query.search.term.replace(/[\\%_]/g, (char) => `\\${char}`)}%`;
1212
+ const searchable = query.search.fields.map((key) => columns[key]);
1213
+ conditions.push(or(...searchable.map((column) => sql`${column} LIKE ${pattern} ESCAPE '\\'`)));
1214
+ }
1215
+ for (const [key, value] of Object.entries(query.filters)) {
1216
+ conditions.push(value === null ? isNull(columns[key]) : eq(columns[key], value));
1217
+ }
1218
+ return conditions;
1219
+ }
1220
+ return {
1221
+ columns,
1222
+ get db() {
1223
+ return getDb();
1224
+ },
1225
+ async find(query) {
1226
+ const conditions = matching(query);
1227
+ const sortColumn = columns[query.sort.field];
1228
+ const descending = query.sort.direction === "desc";
1229
+ const order = descending ? [desc(sortColumn), desc(idColumn)] : [asc(sortColumn), asc(idColumn)];
1230
+ if (query.cursor) {
1231
+ const { value, id, greaterThan } = query.cursor;
1232
+ conditions.push(
1233
+ greaterThan ? or(sql`${sortColumn} > ${value}`, and(sql`${sortColumn} = ${value}`, sql`${idColumn} > ${id}`)) : or(sql`${sortColumn} < ${value}`, and(sql`${sortColumn} = ${value}`, sql`${idColumn} < ${id}`))
1234
+ );
1235
+ }
1236
+ const where = conditions.length ? and(...conditions) : void 0;
1237
+ const statement = getDb().select().from(table).where(where).orderBy(...order).limit(query.limit);
1238
+ const rows = query.offset ? await statement.offset(query.offset) : await statement;
1239
+ return rows;
1240
+ },
1241
+ async countUpTo(query, limit) {
1242
+ const conditions = matching(query);
1243
+ const where = conditions.length ? and(...conditions) : void 0;
1244
+ const db = getDb();
1245
+ const [counted] = await db.select({ total: count() }).from(db.select({ id: idColumn }).from(table).where(where).limit(limit).as("capped"));
1246
+ return counted?.total ?? 0;
1247
+ },
1248
+ async byId(id) {
1249
+ const [record] = await getDb().select().from(table).where(byId(id)).limit(1);
1250
+ return record ?? null;
1251
+ },
1252
+ async titles(ids, titleField) {
1253
+ const titleColumn = columns[titleField] ?? idColumn;
1254
+ const rows = await getDb().select({ id: idColumn, title: titleColumn }).from(table).where(inArray(idColumn, ids));
1255
+ return rows;
1256
+ },
1257
+ async insert(values) {
1258
+ const [record] = await getDb().insert(table).values(values).returning();
1259
+ return record;
1260
+ },
1261
+ async update(id, values) {
1262
+ const [record] = await getDb().update(table).set(values).where(byId(id)).returning();
1263
+ return record ?? null;
1264
+ },
1265
+ async remove(id) {
1266
+ const deleted = await getDb().delete(table).where(byId(id)).returning({ id: idColumn });
1267
+ return deleted.length > 0;
1268
+ },
1269
+ constraint(error) {
1270
+ const text = errorText(error);
1271
+ const unique = /UNIQUE constraint failed: ([\w.]+(?:, [\w.]+)*)/.exec(text);
1272
+ if (unique) return { kind: "unique", column: unique[1].split(", ")[0].split(".").pop() };
1273
+ if (/FOREIGN KEY constraint failed/.test(text)) return { kind: "foreignKey" };
1274
+ if (/CHECK constraint failed/.test(text)) return { kind: "check" };
1275
+ return void 0;
1276
+ }
1277
+ };
1278
+ }
1279
+
1189
1280
  // src/server/query.ts
1190
1281
  function encodeCursor(cursor) {
1191
1282
  const json2 = JSON.stringify([cursor.value, cursor.id, cursor.direction === "before" ? 0 : 1]);
@@ -1284,45 +1375,36 @@ function toCursorValue(value) {
1284
1375
  if (typeof value === "object") return String(value);
1285
1376
  return value;
1286
1377
  }
1287
- function errorText(error) {
1288
- const parts = [];
1289
- for (let current = error, depth = 0; current && depth < 5; depth++) {
1290
- if (current instanceof Error) {
1291
- parts.push(current.message);
1292
- current = current.cause;
1293
- } else {
1294
- parts.push(String(current));
1295
- break;
1296
- }
1297
- }
1298
- return parts.join(" | ");
1299
- }
1300
1378
  function createResourceStore(options) {
1301
- const { resource, table, getDb } = options;
1379
+ const { resource, table } = options;
1302
1380
  const validators = createValidators(resource);
1303
- const columns = getTableColumns(table);
1304
1381
  const fields = storedFields(resource);
1305
- for (const key of ["id", "createdAt", "updatedAt", ...fields.map(([key2]) => key2)]) {
1306
- if (!columns[key]) {
1307
- throw new Error(`Resource "${resource.name}": table has no column for "${key}". Run \`flare sync-types\` and create a migration.`);
1382
+ if (!options.rows && !(table && options.getDb)) {
1383
+ throw new Error(`Resource "${resource.name}": a store needs either a Drizzle table and getDb, or rows.`);
1384
+ }
1385
+ const rows = options.rows ?? drizzleRows(table, options.getDb);
1386
+ const columns = "columns" in rows ? rows.columns : void 0;
1387
+ if (columns) {
1388
+ for (const key of ["id", "createdAt", "updatedAt", ...fields.map(([key2]) => key2)]) {
1389
+ if (!columns[key]) {
1390
+ throw new Error(`Resource "${resource.name}": table has no column for "${key}". Run \`flare sync-types\` and create a migration.`);
1391
+ }
1308
1392
  }
1309
1393
  }
1310
- const idColumn = columns.id;
1311
- const columnKeyByName = new Map(Object.entries(columns).map(([key, column]) => [column.name, key]));
1394
+ const keyForColumn = (name) => columns ? Object.entries(columns).find(([, column]) => column.name === name)?.[0] : ["id", "createdAt", "updatedAt", ...fields.map(([key]) => key)].find((key) => columnName(key) === name || key === name);
1312
1395
  const fail = (status, error, extra = {}) => ({ ok: false, status, error, ...extra });
1313
1396
  function constraintFailure(error, action) {
1314
- const text = errorText(error);
1315
- const unique = /UNIQUE constraint failed: ([\w.]+(?:, [\w.]+)*)/.exec(text);
1316
- if (unique) {
1317
- const key = columnKeyByName.get(unique[1].split(", ")[0].split(".").pop());
1397
+ const hit = rows.constraint(error);
1398
+ if (!hit) return void 0;
1399
+ if (hit.kind === "unique") {
1400
+ const key = hit.column ? keyForColumn(hit.column) : void 0;
1318
1401
  const label = key ? resource.fields[key]?.label ?? key : "Value";
1319
1402
  return fail(409, `${label} is already taken.`, key ? { field: key } : {});
1320
1403
  }
1321
- if (/FOREIGN KEY constraint failed/.test(text)) {
1404
+ if (hit.kind === "foreignKey") {
1322
1405
  return action === "delete" ? fail(409, `This ${resource.label.toLowerCase()} is still referenced by other records.`) : fail(422, "A related record doesn't exist.");
1323
1406
  }
1324
- if (/CHECK constraint failed/.test(text)) return fail(422, "A value is outside its allowed options.");
1325
- return void 0;
1407
+ return fail(422, "A value is outside its allowed options.");
1326
1408
  }
1327
1409
  async function guarded(action, work) {
1328
1410
  try {
@@ -1347,7 +1429,6 @@ function createResourceStore(options) {
1347
1429
  return result;
1348
1430
  }
1349
1431
  const invalid = (issues) => fail(422, "Validation failed.", { issues: issues.map((issue) => ({ path: issue.path.map(String).join("."), message: issue.message })) });
1350
- const byId = (id) => eq(idColumn, id);
1351
1432
  const notFound = () => fail(404, `${resource.label} not found.`);
1352
1433
  const computed = Object.entries(resource.computed ?? {});
1353
1434
  const withComputed = (row) => {
@@ -1363,7 +1444,7 @@ function createResourceStore(options) {
1363
1444
  return result;
1364
1445
  };
1365
1446
  const hooks = resource.hooks ?? {};
1366
- const hookContext = async () => ({ db: getDb(), user: await options.currentUser?.() ?? null });
1447
+ const hookContext = async () => ({ db: rows.db, user: await options.currentUser?.() ?? null });
1367
1448
  return {
1368
1449
  resource,
1369
1450
  table,
@@ -1372,50 +1453,35 @@ function createResourceStore(options) {
1372
1453
  const parsed = parseListQuery(resource, params);
1373
1454
  if ("issues" in parsed) return fail(400, "Invalid query.", { queryIssues: parsed.issues });
1374
1455
  const { page, perPage, sort, q, filters, cursor } = parsed.query;
1375
- const conditions = [];
1376
- if (q) {
1377
- const pattern = `%${q.replace(/[\\%_]/g, (char) => `\\${char}`)}%`;
1378
- const searchable = fields.filter(([, def]) => isSearchable(def)).map(([key]) => columns[key]);
1379
- if (searchable.length) conditions.push(or(...searchable.map((column) => sql`${column} LIKE ${pattern} ESCAPE '\\'`)));
1380
- }
1381
- for (const [key, value] of Object.entries(filters)) {
1382
- conditions.push(value === null ? isNull(columns[key]) : eq(columns[key], value));
1383
- }
1384
- const filterWhere = conditions.length ? and(...conditions) : void 0;
1385
- const sortColumn = columns[sort.field];
1456
+ const searchable = fields.filter(([, def]) => isSearchable(def)).map(([key]) => key);
1457
+ const matching = { search: q ? { term: q, fields: searchable } : void 0, filters };
1386
1458
  const backwards = cursor?.direction === "before";
1387
1459
  const descending = backwards ? sort.direction === "asc" : sort.direction === "desc";
1388
- const order = descending ? [desc(sortColumn), desc(idColumn)] : [asc(sortColumn), asc(idColumn)];
1389
- const pageConditions = [...conditions];
1390
- if (cursor) {
1391
- const value = cursor.value;
1392
- pageConditions.push(
1393
- descending ? or(sql`${sortColumn} < ${value}`, and(sql`${sortColumn} = ${value}`, sql`${idColumn} < ${cursor.id}`)) : or(sql`${sortColumn} > ${value}`, and(sql`${sortColumn} = ${value}`, sql`${idColumn} > ${cursor.id}`))
1394
- );
1395
- }
1396
- const where = pageConditions.length ? and(...pageConditions) : void 0;
1397
- const db = getDb();
1398
- const query = db.select().from(table).where(where).orderBy(...order).limit(perPage + 1);
1399
- const [found, counted] = await Promise.all([
1400
- cursor ? query : query.offset((page - 1) * perPage),
1460
+ const [found, counting] = await Promise.all([
1401
1461
  // One row past the limit is enough to know there's more without counting it all.
1402
- db.select({ total: count() }).from(db.select({ id: idColumn }).from(table).where(filterWhere).limit(COUNT_LIMIT + 1).as("capped"))
1462
+ rows.find({
1463
+ ...matching,
1464
+ sort: { field: sort.field, direction: descending ? "desc" : "asc" },
1465
+ cursor: cursor ? { field: sort.field, value: cursor.value, id: cursor.id, greaterThan: !descending } : void 0,
1466
+ limit: perPage + 1,
1467
+ offset: cursor ? void 0 : (page - 1) * perPage
1468
+ }),
1469
+ rows.countUpTo(matching, COUNT_LIMIT + 1)
1403
1470
  ]);
1404
1471
  const hasMore = found.length > perPage;
1405
- const rows = (hasMore ? found.slice(0, perPage) : found).map(withComputed);
1406
- if (backwards) rows.reverse();
1407
- const counting = counted[0]?.total ?? 0;
1472
+ const page_ = (hasMore ? found.slice(0, perPage) : found).map(withComputed);
1473
+ if (backwards) page_.reverse();
1408
1474
  const exactTotal = counting <= COUNT_LIMIT;
1409
1475
  const total = exactTotal ? counting : COUNT_LIMIT;
1410
- const first = rows[0];
1411
- const last = rows[rows.length - 1];
1476
+ const first = page_[0];
1477
+ const last = page_[page_.length - 1];
1412
1478
  const moreAfter = backwards ? true : hasMore;
1413
1479
  const moreBefore = backwards ? hasMore : Boolean(cursor) || page > 1;
1414
1480
  const cursorFor = (row, direction) => row ? encodeCursor({ value: toCursorValue(row[sort.field]), id: String(row.id), direction }) : void 0;
1415
1481
  return {
1416
1482
  ok: true,
1417
1483
  data: {
1418
- data: rows,
1484
+ data: page_,
1419
1485
  meta: {
1420
1486
  page,
1421
1487
  perPage,
@@ -1429,40 +1495,40 @@ function createResourceStore(options) {
1429
1495
  };
1430
1496
  },
1431
1497
  async get(id) {
1432
- const [record] = await getDb().select().from(table).where(byId(id)).limit(1);
1498
+ const record = await rows.byId(id);
1433
1499
  return record ? { ok: true, data: withComputed(record) } : notFound();
1434
1500
  },
1435
1501
  /** Title-field values for a set of ids (for showing relations). Missing ids are omitted. */
1436
1502
  async titles(ids) {
1437
1503
  const unique = [...new Set(ids.filter(Boolean))];
1438
1504
  if (unique.length === 0) return {};
1439
- const titleColumn = columns[resource.titleField] ?? idColumn;
1440
- const rows = await getDb().select({ id: idColumn, title: titleColumn }).from(table).where(inArray(idColumn, unique));
1441
- return Object.fromEntries(rows.map((row) => [row.id, row.title == null ? row.id : String(row.title)]));
1505
+ const found = await rows.titles(unique, resource.titleField);
1506
+ return Object.fromEntries(found.map((row) => [row.id, row.title == null ? row.id : String(row.title)]));
1442
1507
  },
1443
- create(input) {
1444
- const result = validators.create.safeParse(input);
1445
- if (!result.success) return Promise.resolve(invalid(result.error.issues));
1508
+ create(raw) {
1446
1509
  return writing("create", async () => {
1447
1510
  const context = await hookContext();
1448
- const input2 = hooks.beforeCreate ? await hooks.beforeCreate({ ...result.data }, context) : result.data;
1511
+ const supplied = hooks.beforeCreate ? await hooks.beforeCreate({ ...raw ?? {} }, context) : raw;
1512
+ const result = validators.create.safeParse(supplied);
1513
+ if (!result.success) return invalid(result.error.issues);
1514
+ const input = result.data;
1449
1515
  const now = /* @__PURE__ */ new Date();
1450
- const [record] = await getDb().insert(table).values({ ...input2, id: crypto.randomUUID(), createdAt: now, updatedAt: now }).returning();
1516
+ const record = await rows.insert({ ...input, id: crypto.randomUUID(), createdAt: now, updatedAt: now });
1451
1517
  const saved = withComputed(record);
1452
1518
  await hooks.afterCreate?.(saved, context);
1453
1519
  return { ok: true, data: saved };
1454
1520
  });
1455
1521
  },
1456
1522
  /** Partial update (PATCH semantics). */
1457
- update(id, input) {
1458
- const result = validators.update.safeParse(input);
1459
- if (!result.success) return Promise.resolve(invalid(result.error.issues));
1523
+ update(id, raw) {
1460
1524
  return writing("update", async () => {
1461
1525
  const context = await hookContext();
1462
- const [existing] = hooks.beforeUpdate || hooks.afterUpdate ? await getDb().select().from(table).where(byId(id)).limit(1) : [];
1463
- const current = existing ?? null;
1464
- const input2 = hooks.beforeUpdate ? await hooks.beforeUpdate({ ...result.data }, { ...context, id, current }) : result.data;
1465
- const [record] = await getDb().update(table).set({ ...input2, updatedAt: /* @__PURE__ */ new Date() }).where(byId(id)).returning();
1526
+ const current = hooks.beforeUpdate || hooks.afterUpdate ? await rows.byId(id) : null;
1527
+ const supplied = hooks.beforeUpdate ? await hooks.beforeUpdate({ ...raw ?? {} }, { ...context, id, current }) : raw;
1528
+ const result = validators.update.safeParse(supplied);
1529
+ if (!result.success) return invalid(result.error.issues);
1530
+ const input = result.data;
1531
+ const record = await rows.update(id, { ...input, updatedAt: /* @__PURE__ */ new Date() });
1466
1532
  if (!record) return notFound();
1467
1533
  const saved = withComputed(record);
1468
1534
  await hooks.afterUpdate?.(saved, { ...context, previous: current });
@@ -1479,7 +1545,7 @@ function createResourceStore(options) {
1479
1545
  values[key] = "default" in def && def.default !== void 0 ? def.default : def.required ? void 0 : null;
1480
1546
  }
1481
1547
  Object.assign(values, result.data, { updatedAt: /* @__PURE__ */ new Date() });
1482
- const [record] = await getDb().update(table).set(values).where(byId(id)).returning();
1548
+ const record = await rows.update(id, values);
1483
1549
  return record ? { ok: true, data: withComputed(record) } : notFound();
1484
1550
  });
1485
1551
  },
@@ -1487,11 +1553,9 @@ function createResourceStore(options) {
1487
1553
  return writing("delete", async () => {
1488
1554
  const context = await hookContext();
1489
1555
  if (hooks.beforeDelete) {
1490
- const [existing] = await getDb().select().from(table).where(byId(id)).limit(1);
1491
- await hooks.beforeDelete({ ...context, id, current: existing ?? null });
1556
+ await hooks.beforeDelete({ ...context, id, current: await rows.byId(id) });
1492
1557
  }
1493
- const deleted = await getDb().delete(table).where(byId(id)).returning({ id: idColumn });
1494
- if (deleted.length === 0) return notFound();
1558
+ if (!await rows.remove(id)) return notFound();
1495
1559
  await hooks.afterDelete?.({ ...context, id });
1496
1560
  return { ok: true, data: { id } };
1497
1561
  });
@@ -1584,6 +1648,99 @@ function createResourceHandlers(options) {
1584
1648
  };
1585
1649
  }
1586
1650
 
1651
+ // src/server/prisma-rows.ts
1652
+ var NOT_FOUND = "P2025";
1653
+ var codeOf = (error) => typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
1654
+ function uniqueColumn(error) {
1655
+ const meta = error.meta;
1656
+ if (!meta) return void 0;
1657
+ const target = meta.target;
1658
+ if (Array.isArray(target) && target.length > 0) return String(target[0]);
1659
+ if (typeof target === "string") return fromIndexName(target);
1660
+ const cause = meta.driverAdapterError?.cause;
1661
+ const constraint = cause?.constraint;
1662
+ if (constraint?.fields?.length) return constraint.fields[0];
1663
+ if (constraint?.index) return fromIndexName(constraint.index);
1664
+ return void 0;
1665
+ }
1666
+ function fromIndexName(name) {
1667
+ const match = /^.*?_(.+)_key$/.exec(name);
1668
+ return match ? match[1] : name;
1669
+ }
1670
+ function prismaRows(delegate, client) {
1671
+ function matching(query) {
1672
+ const where = { ...query.filters };
1673
+ if (query.search && query.search.fields.length > 0) {
1674
+ where.OR = query.search.fields.map((field) => ({ [field]: { contains: query.search.term, mode: "insensitive" } }));
1675
+ }
1676
+ return where;
1677
+ }
1678
+ return {
1679
+ db: client,
1680
+ async find(query) {
1681
+ const where = matching(query);
1682
+ if (query.cursor) {
1683
+ const { field, value, id, greaterThan } = query.cursor;
1684
+ const op = greaterThan ? "gt" : "lt";
1685
+ const keyset = [{ [field]: { [op]: value } }, { AND: [{ [field]: value }, { id: { [op]: id } }] }];
1686
+ where.AND = [...where.AND ?? [], { OR: keyset }];
1687
+ }
1688
+ return delegate.findMany({
1689
+ where,
1690
+ orderBy: [{ [query.sort.field]: query.sort.direction }, { id: query.sort.direction }],
1691
+ take: query.limit,
1692
+ ...query.offset ? { skip: query.offset } : {}
1693
+ });
1694
+ },
1695
+ countUpTo(query, limit) {
1696
+ return delegate.count({ where: matching(query), take: limit });
1697
+ },
1698
+ byId(id) {
1699
+ return delegate.findUnique({ where: { id } });
1700
+ },
1701
+ async titles(ids, titleField) {
1702
+ const rows = await delegate.findMany({ where: { id: { in: ids } }, select: { id: true, [titleField]: true } });
1703
+ return rows.map((row) => ({ id: String(row.id), title: row[titleField] }));
1704
+ },
1705
+ insert(values) {
1706
+ return delegate.create({ data: values });
1707
+ },
1708
+ async update(id, values) {
1709
+ try {
1710
+ return await delegate.update({ where: { id }, data: values });
1711
+ } catch (error) {
1712
+ if (codeOf(error) === NOT_FOUND) return null;
1713
+ throw error;
1714
+ }
1715
+ },
1716
+ async remove(id) {
1717
+ try {
1718
+ await delegate.delete({ where: { id } });
1719
+ return true;
1720
+ } catch (error) {
1721
+ if (codeOf(error) === NOT_FOUND) return false;
1722
+ throw error;
1723
+ }
1724
+ },
1725
+ constraint(error) {
1726
+ switch (codeOf(error)) {
1727
+ case "P2002":
1728
+ return { kind: "unique", column: uniqueColumn(error) };
1729
+ case "P2003":
1730
+ case "P2014":
1731
+ return { kind: "foreignKey" };
1732
+ // A value outside its type, too long for its column, or null where it can't be.
1733
+ case "P2000":
1734
+ case "P2011":
1735
+ case "P2020":
1736
+ return { kind: "check" };
1737
+ default:
1738
+ return void 0;
1739
+ }
1740
+ }
1741
+ };
1742
+ }
1743
+
1587
1744
  // src/server/openapi.ts
1588
1745
  var E164 = "^\\+[1-9]\\d{6,14}$";
1589
1746
  function fieldJsonSchema(def) {
@@ -1857,10 +2014,12 @@ function openApiDocument(options) {
1857
2014
  export {
1858
2015
  createResourceHandlers,
1859
2016
  createResourceStore,
2017
+ drizzleRows,
1860
2018
  fieldJsonSchema,
1861
2019
  isFilterable,
1862
2020
  isSearchable,
1863
2021
  isSortable,
1864
2022
  openApiDocument,
1865
- parseListQuery
2023
+ parseListQuery,
2024
+ prismaRows
1866
2025
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flaredev/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Flare runtime: resource descriptors, field grammar, and Cloudflare-native helpers",
5
5
  "license": "MIT",
6
6
  "author": "Muke Johnbaptist",