@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/chunk-7NQIVWGG.js +68 -0
- package/dist/{chunk-62JI5DCR.js → chunk-BTVEH5AU.js} +342 -8
- package/dist/chunk-I2HSIPCF.js +65 -0
- package/dist/index.d.ts +121 -5
- package/dist/index.js +71 -217
- package/dist/{policy-DaK3dWQ_.d.ts → policy-b4FXN3ZJ.d.ts} +147 -2
- package/dist/rate-limit/server.d.ts +44 -0
- package/dist/rate-limit/server.js +45 -0
- package/dist/rate-limit-ClbdZioJ.d.ts +97 -0
- package/dist/security/worker.js +13 -5
- package/dist/server.d.ts +69 -62
- package/dist/server.js +116 -37
- package/package.json +5 -1
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-
|
|
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
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
|
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(
|
|
1433
|
-
|
|
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)
|
|
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 =
|
|
1574
|
-
|
|
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))
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1900
|
-
|
|
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.
|
|
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"
|