@flaredev/core 0.11.0 → 0.12.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.
package/dist/server.js CHANGED
@@ -2,9 +2,17 @@ import {
2
2
  can,
3
3
  columnName,
4
4
  createValidators,
5
+ enumTranslations,
5
6
  moneyExponent,
6
7
  storedFields
7
- } from "./chunk-62JI5DCR.js";
8
+ } from "./chunk-BTVEH5AU.js";
9
+ import {
10
+ ISSUE_CODES,
11
+ PROBLEM_CODES,
12
+ problemResponse,
13
+ wantsJson
14
+ } from "./chunk-7NQIVWGG.js";
15
+ import "./chunk-I2HSIPCF.js";
8
16
 
9
17
  // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@cloudfl_21866ab18c10d2d988b916e06105fbce/node_modules/drizzle-orm/entity.js
10
18
  var entityKind = /* @__PURE__ */ Symbol.for("drizzle:entityKind");
@@ -1293,7 +1301,7 @@ function decodeCursor(raw) {
1293
1301
  return void 0;
1294
1302
  }
1295
1303
  }
1296
- var TIMESTAMP_FIELDS = ["createdAt", "updatedAt"];
1304
+ var SYSTEM_SORT_FIELDS = ["id", "createdAt", "updatedAt"];
1297
1305
  var MAX_PER_PAGE = 100;
1298
1306
  var MAX_SEARCH_LENGTH = 200;
1299
1307
  function isSortable(def) {
@@ -1341,7 +1349,7 @@ function parseListQuery(resource, params, options = {}) {
1341
1349
  const direction = rawSort.startsWith("-") ? "desc" : "asc";
1342
1350
  const field = rawSort.replace(/^-/, "");
1343
1351
  const def = fields.get(field);
1344
- if (TIMESTAMP_FIELDS.includes(field) || def && isSortable(def)) sort = { field, direction };
1352
+ if (SYSTEM_SORT_FIELDS.includes(field) || def && isSortable(def)) sort = { field, direction };
1345
1353
  else issues.push({ param: "sort", message: `can't sort by "${field}"` });
1346
1354
  }
1347
1355
  const q = params.get("q")?.trim().slice(0, MAX_SEARCH_LENGTH) || void 0;
@@ -1393,19 +1401,26 @@ function createResourceStore(options) {
1393
1401
  }
1394
1402
  }
1395
1403
  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);
1396
- const fail = (status, error, extra = {}) => ({ ok: false, status, error, ...extra });
1404
+ const fail = (status, error, code, issues) => ({
1405
+ ok: false,
1406
+ status,
1407
+ error,
1408
+ code,
1409
+ ...issues?.length ? { issues } : {}
1410
+ });
1397
1411
  function constraintFailure(error, action) {
1398
1412
  const hit = rows.constraint(error);
1399
1413
  if (!hit) return void 0;
1400
1414
  if (hit.kind === "unique") {
1401
1415
  const key = hit.column ? keyForColumn(hit.column) : void 0;
1402
1416
  const label = key ? resource.fields[key]?.label ?? key : "Value";
1403
- return fail(409, `${label} is already taken.`, key ? { field: key } : {});
1417
+ const error2 = `${label} is already taken.`;
1418
+ return fail(409, error2, PROBLEM_CODES.uniqueViolation, key ? [{ path: key, message: error2, code: ISSUE_CODES.unique }] : void 0);
1404
1419
  }
1405
1420
  if (hit.kind === "foreignKey") {
1406
- return action === "delete" ? fail(409, `This ${resource.label.toLowerCase()} is still referenced by other records.`) : fail(422, "A related record doesn't exist.");
1421
+ return action === "delete" ? fail(409, `This ${resource.label.toLowerCase()} is still referenced by other records.`, PROBLEM_CODES.stillReferenced) : fail(422, "A related record doesn't exist.", PROBLEM_CODES.relatedRecordMissing);
1407
1422
  }
1408
- return fail(422, "A value is outside its allowed options.");
1423
+ return fail(422, "A value is outside its allowed options.", PROBLEM_CODES.valueNotAllowed);
1409
1424
  }
1410
1425
  async function guarded(action, work) {
1411
1426
  try {
@@ -1429,8 +1444,13 @@ function createResourceStore(options) {
1429
1444
  await announce(action, result);
1430
1445
  return result;
1431
1446
  }
1432
- const invalid = (issues) => fail(422, "Validation failed.", { issues: issues.map((issue) => ({ path: issue.path.map(String).join("."), message: issue.message })) });
1433
- const notFound = () => fail(404, `${resource.label} not found.`);
1447
+ const invalid = (issues) => fail(
1448
+ 422,
1449
+ "Validation failed.",
1450
+ PROBLEM_CODES.validationFailed,
1451
+ issues.map((issue) => ({ path: issue.path.map(String).join("."), message: issue.message, code: ISSUE_CODES.invalid }))
1452
+ );
1453
+ const notFound = () => fail(404, `${resource.label} not found.`, PROBLEM_CODES.notFound);
1434
1454
  const computed = Object.entries(resource.computed ?? {});
1435
1455
  const withComputed = (row) => {
1436
1456
  if (computed.length === 0) return row;
@@ -1452,7 +1472,14 @@ function createResourceStore(options) {
1452
1472
  /** `params`: page, perPage, sort, q, filter[field], cursor (as parsed by parseListQuery). */
1453
1473
  async list(params, options2 = {}) {
1454
1474
  const parsed = parseListQuery(resource, params, options2);
1455
- if ("issues" in parsed) return fail(400, "Invalid query.", { queryIssues: parsed.issues });
1475
+ if ("issues" in parsed) {
1476
+ return fail(
1477
+ 400,
1478
+ "Invalid query.",
1479
+ PROBLEM_CODES.invalidQuery,
1480
+ parsed.issues.map((issue) => ({ path: issue.param, message: issue.message, code: ISSUE_CODES.invalidParameter }))
1481
+ );
1482
+ }
1456
1483
  const { page, perPage, sort, q, filters, cursor } = parsed.query;
1457
1484
  const isDateField = (key) => {
1458
1485
  if (key === "createdAt" || key === "updatedAt") return true;
@@ -1570,14 +1597,8 @@ function createResourceStore(options) {
1570
1597
  }
1571
1598
 
1572
1599
  // src/server/handlers.ts
1573
- var problem = (status, error, extra = {}) => Response.json({ error, ...extra }, { status });
1574
- function failureResponse(failure) {
1575
- if (failure.queryIssues) return problem(failure.status, failure.error, { issues: failure.queryIssues });
1576
- const extra = {};
1577
- if (failure.issues) extra.issues = failure.issues;
1578
- if (failure.field) extra.field = failure.field;
1579
- return problem(failure.status, failure.error, extra);
1580
- }
1600
+ var problem = problemResponse;
1601
+ var failureResponse = (failure) => problemResponse(failure.status, failure.error, failure.code, failure.issues);
1581
1602
  var respond = (result, init) => result.ok ? Response.json(result.data, init) : failureResponse(result);
1582
1603
  function createResourceHandlers(options) {
1583
1604
  const { resource, authorize } = options;
@@ -1587,13 +1608,15 @@ function createResourceHandlers(options) {
1587
1608
  return origin !== null && origin !== new URL(request.url).origin;
1588
1609
  };
1589
1610
  async function readBody(request) {
1590
- if (crossOrigin(request)) return { response: problem(403, "Cross-origin request blocked.") };
1611
+ if (crossOrigin(request)) return { response: problem(403, "Cross-origin request blocked.", PROBLEM_CODES.crossOriginBlocked) };
1591
1612
  const type = request.headers.get("content-type") ?? "";
1592
- if (!/^application\/json\b/i.test(type)) return { response: problem(415, "Send JSON with Content-Type: application/json.") };
1613
+ if (!/^application\/json\b/i.test(type)) {
1614
+ return { response: problem(415, "Send JSON with Content-Type: application/json.", PROBLEM_CODES.unsupportedMediaType) };
1615
+ }
1593
1616
  try {
1594
1617
  return { body: await request.json() };
1595
1618
  } catch {
1596
- return { response: problem(400, "Request body isn't valid JSON.") };
1619
+ return { response: problem(400, "Request body isn't valid JSON.", PROBLEM_CODES.invalidJson) };
1597
1620
  }
1598
1621
  }
1599
1622
  const guard = async (context) => await authorize(context) ?? void 0;
@@ -1637,7 +1660,7 @@ function createResourceHandlers(options) {
1637
1660
  PATCH: (request, context) => item(request, context, "update", (id) => withBody(request, async (body) => respond(await store.update(id, body)))),
1638
1661
  PUT: (request, context) => item(request, context, "update", (id) => withBody(request, async (body) => respond(await store.replace(id, body)))),
1639
1662
  DELETE: (request, context) => item(request, context, "delete", async (id) => {
1640
- if (crossOrigin(request)) return problem(403, "Cross-origin request blocked.");
1663
+ if (crossOrigin(request)) return problem(403, "Cross-origin request blocked.", PROBLEM_CODES.crossOriginBlocked);
1641
1664
  const result = await store.delete(id);
1642
1665
  return result.ok ? new Response(null, { status: 204 }) : failureResponse(result);
1643
1666
  })
@@ -1673,9 +1696,25 @@ function fromIndexName(name) {
1673
1696
  const match = /^.*?_(.+)_key$/.exec(name);
1674
1697
  return match ? match[1] : name;
1675
1698
  }
1676
- function prismaRows(delegate, client) {
1699
+ function translate(row, enums, direction) {
1700
+ let copy;
1701
+ for (const key of Object.keys(enums)) {
1702
+ const current = row[key];
1703
+ if (typeof current !== "string") continue;
1704
+ const mapped = enums[key][direction][current];
1705
+ if (mapped === void 0 || mapped === current) continue;
1706
+ copy ??= { ...row };
1707
+ copy[key] = mapped;
1708
+ }
1709
+ return copy ?? row;
1710
+ }
1711
+ function prismaRows(delegate, client, resource) {
1712
+ const enums = resource ? enumTranslations(storedFields(resource)) : {};
1713
+ const hasEnums = Object.keys(enums).length > 0;
1714
+ const toValue = (row) => hasEnums ? translate(row, enums, "toValue") : row;
1715
+ const toMember = (row) => hasEnums ? translate(row, enums, "toMember") : row;
1677
1716
  function matching(query) {
1678
- const where = { ...query.filters };
1717
+ const where = hasEnums ? { ...toMember(query.filters) } : { ...query.filters };
1679
1718
  if (query.search && query.search.fields.length > 0) {
1680
1719
  where.OR = query.search.fields.map((field) => ({ [field]: { contains: query.search.term, mode: "insensitive" } }));
1681
1720
  }
@@ -1688,33 +1727,37 @@ function prismaRows(delegate, client) {
1688
1727
  if (query.cursor) {
1689
1728
  const { field, id, greaterThan, isDate } = query.cursor;
1690
1729
  const op = greaterThan ? "gt" : "lt";
1691
- const value = isDate && (typeof query.cursor.value === "number" || typeof query.cursor.value === "string") ? new Date(query.cursor.value) : query.cursor.value;
1730
+ let value = isDate && (typeof query.cursor.value === "number" || typeof query.cursor.value === "string") ? new Date(query.cursor.value) : query.cursor.value;
1731
+ if (hasEnums && enums[field] && typeof value === "string") value = enums[field].toMember[value] ?? value;
1692
1732
  const keyset = [{ [field]: { [op]: value } }, { AND: [{ [field]: value }, { id: { [op]: id } }] }];
1693
1733
  where.AND = [...where.AND ?? [], { OR: keyset }];
1694
1734
  }
1695
- return delegate.findMany({
1735
+ const rows = await delegate.findMany({
1696
1736
  where,
1697
1737
  orderBy: [{ [query.sort.field]: query.sort.direction }, { id: query.sort.direction }],
1698
1738
  take: query.limit,
1699
1739
  ...query.offset ? { skip: query.offset } : {}
1700
1740
  });
1741
+ return hasEnums ? rows.map(toValue) : rows;
1701
1742
  },
1702
1743
  countUpTo(query, limit) {
1703
1744
  return delegate.count({ where: matching(query), take: limit });
1704
1745
  },
1705
- byId(id) {
1706
- return delegate.findUnique({ where: { id } });
1746
+ async byId(id) {
1747
+ const row = await delegate.findUnique({ where: { id } });
1748
+ return row ? toValue(row) : row;
1707
1749
  },
1708
1750
  async titles(ids, titleField) {
1709
1751
  const rows = await delegate.findMany({ where: { id: { in: ids } }, select: { id: true, [titleField]: true } });
1710
1752
  return rows.map((row) => ({ id: String(row.id), title: row[titleField] }));
1711
1753
  },
1712
- insert(values) {
1713
- return delegate.create({ data: values });
1754
+ async insert(values) {
1755
+ return toValue(await delegate.create({ data: hasEnums ? toMember(values) : values }));
1714
1756
  },
1715
1757
  async update(id, values) {
1716
1758
  try {
1717
- return await delegate.update({ where: { id }, data: values });
1759
+ const row = await delegate.update({ where: { id }, data: hasEnums ? toMember(values) : values });
1760
+ return toValue(row);
1718
1761
  } catch (error) {
1719
1762
  if (codeOf(error) === NOT_FOUND) return null;
1720
1763
  throw error;
@@ -1773,6 +1816,20 @@ function fieldJsonSchema(def) {
1773
1816
  return { ...schema, pattern: "^#[0-9a-f]{6}$", description: "Hex colour.", examples: ["#f2541d"] };
1774
1817
  case "slug":
1775
1818
  return { ...schema, pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$", examples: ["my-first-post"] };
1819
+ case "username":
1820
+ return { ...schema, pattern: "^[A-Za-z0-9_][A-Za-z0-9_.]{1,31}$", description: "Username: 2\u201332 characters (letters, digits, underscore, dot).", examples: ["ada_lovelace"] };
1821
+ case "ip":
1822
+ return { ...schema, description: "An IPv4 or IPv6 address.", examples: ["192.168.1.1", "2001:db8::1"] };
1823
+ case "uuid":
1824
+ return { ...schema, format: "uuid", description: "A UUID, any version.", examples: ["123e4567-e89b-12d3-a456-426614174000"] };
1825
+ case "timezone":
1826
+ return { ...schema, description: "An IANA time zone identifier.", examples: ["Africa/Kampala", "America/New_York"] };
1827
+ case "locale":
1828
+ return { ...schema, pattern: "^[A-Za-z]{2,3}(?:-[A-Za-z]{4})?(?:-(?:[A-Za-z]{2}|\\d{3}))?$", description: "A BCP-47 language tag.", examples: ["en-GB", "sw-UG"] };
1829
+ case "currency":
1830
+ return { ...schema, pattern: "^[A-Z]{3}$", description: "ISO 4217 currency code.", examples: ["UGX", "USD"] };
1831
+ case "postcode":
1832
+ return { ...schema, pattern: "^[A-Za-z0-9][A-Za-z0-9 -]{0,10}[A-Za-z0-9]$", description: "Postal code.", examples: ["SW1A 1AA", "90210"] };
1776
1833
  default:
1777
1834
  return schema;
1778
1835
  }
@@ -1867,7 +1924,7 @@ function rolesNote(policy, action) {
1867
1924
  }
1868
1925
  function listParameters(resource) {
1869
1926
  const fields = storedFields(resource);
1870
- const sortable = ["createdAt", "updatedAt", ...fields.filter(([, def]) => isSortable(def)).map(([key]) => key)];
1927
+ const sortable = ["id", "createdAt", "updatedAt", ...fields.filter(([, def]) => isSortable(def)).map(([key]) => key)];
1871
1928
  const parameters = [
1872
1929
  { name: "page", in: "query", schema: { type: "integer", minimum: 1, default: 1 } },
1873
1930
  { name: "perPage", in: "query", schema: { type: "integer", minimum: 1, maximum: 100, default: resource.perPage } },
@@ -1892,14 +1949,32 @@ function openApiDocument(options) {
1892
1949
  const allowed = (resource, action) => visibility === "public" || role !== null && can(policies[resource.name], role, action);
1893
1950
  const paths = {};
1894
1951
  const schemas = {
1952
+ // One shape for every error, whatever refused the request.
1895
1953
  Problem: {
1896
1954
  type: "object",
1897
1955
  properties: {
1898
- error: { type: "string", examples: ["Validation failed."] },
1899
- issues: { type: "array", items: { type: "object", properties: { path: { type: "string" }, message: { type: "string" } } } },
1900
- field: { type: "string", description: "The field a uniqueness conflict is about." }
1956
+ error: { type: "string", description: "The sentence to show a person. Its wording can change; do not branch on it.", examples: ["Validation failed."] },
1957
+ code: {
1958
+ type: "string",
1959
+ description: "Why, as a stable value to switch on.",
1960
+ enum: Object.values(PROBLEM_CODES),
1961
+ examples: ["validation_failed"]
1962
+ },
1963
+ issues: {
1964
+ type: "array",
1965
+ description: "Present whenever the answer is about fields \u2014 one per failed rule, per conflicting column, or per bad query parameter.",
1966
+ items: {
1967
+ type: "object",
1968
+ properties: {
1969
+ path: { type: "string", description: "Field key, dotted for a nested one, or the query parameter's name." },
1970
+ message: { type: "string" },
1971
+ code: { type: "string", enum: Object.values(ISSUE_CODES) }
1972
+ },
1973
+ required: ["path", "message"]
1974
+ }
1975
+ }
1901
1976
  },
1902
- required: ["error"]
1977
+ required: ["error", "code"]
1903
1978
  },
1904
1979
  PageMeta: {
1905
1980
  type: "object",
@@ -2039,6 +2114,8 @@ function openApiDocument(options) {
2039
2114
  };
2040
2115
  }
2041
2116
  export {
2117
+ ISSUE_CODES,
2118
+ PROBLEM_CODES,
2042
2119
  createResourceHandlers,
2043
2120
  createResourceStore,
2044
2121
  drizzleRows,
@@ -2048,5 +2125,7 @@ export {
2048
2125
  isSortable,
2049
2126
  openApiDocument,
2050
2127
  parseListQuery,
2051
- prismaRows
2128
+ prismaRows,
2129
+ problemResponse,
2130
+ wantsJson
2052
2131
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flaredev/core",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "Flare runtime: resource descriptors, field grammar, and Cloudflare-native helpers",
5
5
  "license": "MIT",
6
6
  "author": "Muke Johnbaptist",
@@ -56,6 +56,10 @@
56
56
  "types": "./dist/security/index.d.ts",
57
57
  "import": "./dist/security/index.js"
58
58
  },
59
+ "./rate-limit/server": {
60
+ "types": "./dist/rate-limit/server.d.ts",
61
+ "import": "./dist/rate-limit/server.js"
62
+ },
59
63
  "./security/server": {
60
64
  "types": "./dist/security/worker.d.ts",
61
65
  "import": "./dist/security/worker.js"