@xnetjs/data 0.3.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.
@@ -16,7 +16,7 @@ import {
16
16
  } from "./chunk-QWFTRZQT.js";
17
17
  import {
18
18
  DatabaseRowSchema
19
- } from "./chunk-DWEXKD6E.js";
19
+ } from "./chunk-FB63TZVI.js";
20
20
  import {
21
21
  DatabaseFieldSchema
22
22
  } from "./chunk-KQAT4XBL.js";
@@ -25,7 +25,7 @@ import {
25
25
  } from "./chunk-6OMG4I7M.js";
26
26
  import {
27
27
  DatabaseViewSchema
28
- } from "./chunk-H4GA4BBK.js";
28
+ } from "./chunk-MD553LDL.js";
29
29
  import {
30
30
  createNodeQueryDescriptor
31
31
  } from "./chunk-S5RP5RKY.js";
@@ -85,7 +85,7 @@ function isCellValue(value) {
85
85
 
86
86
  // src/database/row-operations.ts
87
87
  async function createRow(store, options) {
88
- const { databaseId, cells = {}, before, after } = options;
88
+ const { databaseId, cells = {}, before, after, id, submissionMeta } = options;
89
89
  let sortKey;
90
90
  if (!before && !after) {
91
91
  const { rows } = await queryRows(store, databaseId, { limit: 1, sortDirection: "desc" });
@@ -104,10 +104,12 @@ async function createRow(store, options) {
104
104
  {
105
105
  type: "create",
106
106
  options: {
107
+ ...id ? { id } : {},
107
108
  schemaId: DatabaseRowSchema.schema["@id"],
108
109
  properties: {
109
110
  database: databaseId,
110
111
  sortKey,
112
+ ...submissionMeta ? { submissionMeta } : {},
111
113
  ...dynamicProperties
112
114
  }
113
115
  }
@@ -1286,6 +1288,296 @@ function requiresDateColumn(type) {
1286
1288
  return type === "calendar" || type === "timeline";
1287
1289
  }
1288
1290
 
1291
+ // src/database/filter-engine.ts
1292
+ function filterRows(rows, columns, filter) {
1293
+ if (!filter || filter.conditions.length === 0) {
1294
+ return rows;
1295
+ }
1296
+ return rows.filter((row) => evaluateGroup(row, columns, filter));
1297
+ }
1298
+ function evaluateGroup(row, columns, group) {
1299
+ if (group.conditions.length === 0) {
1300
+ return true;
1301
+ }
1302
+ const results = group.conditions.map((condition) => {
1303
+ if (isFilterGroup(condition)) {
1304
+ return evaluateGroup(row, columns, condition);
1305
+ }
1306
+ return evaluateCondition(row, columns, condition);
1307
+ });
1308
+ if (group.operator === "and") {
1309
+ return results.every(Boolean);
1310
+ } else {
1311
+ return results.some(Boolean);
1312
+ }
1313
+ }
1314
+ function evaluateCondition(row, columns, condition) {
1315
+ const column = columns.find((c) => c.id === condition.columnId);
1316
+ if (!column) {
1317
+ return true;
1318
+ }
1319
+ const cellValue = row.cells[condition.columnId];
1320
+ const filterValue = condition.value;
1321
+ return evaluateOperator(cellValue, filterValue, condition.operator, column.type);
1322
+ }
1323
+ function evaluateOperator(cellValue, filterValue, operator, columnType) {
1324
+ switch (operator) {
1325
+ // ─── Equality ─────────────────────────────────────────────────────────────
1326
+ case "equals":
1327
+ return cellValue === filterValue;
1328
+ case "notEquals":
1329
+ return cellValue !== filterValue;
1330
+ // ─── Text Operators ───────────────────────────────────────────────────────
1331
+ case "contains":
1332
+ if (typeof cellValue === "string") {
1333
+ return cellValue.toLowerCase().includes(String(filterValue).toLowerCase());
1334
+ }
1335
+ if (Array.isArray(cellValue)) {
1336
+ return cellValue.includes(filterValue);
1337
+ }
1338
+ return false;
1339
+ case "notContains":
1340
+ return !evaluateOperator(cellValue, filterValue, "contains", columnType);
1341
+ case "startsWith":
1342
+ return typeof cellValue === "string" && cellValue.toLowerCase().startsWith(String(filterValue).toLowerCase());
1343
+ case "endsWith":
1344
+ return typeof cellValue === "string" && cellValue.toLowerCase().endsWith(String(filterValue).toLowerCase());
1345
+ // ─── Empty Operators ──────────────────────────────────────────────────────
1346
+ case "isEmpty":
1347
+ return cellValue === null || cellValue === void 0 || cellValue === "" || Array.isArray(cellValue) && cellValue.length === 0;
1348
+ case "isNotEmpty":
1349
+ return !evaluateOperator(cellValue, null, "isEmpty", columnType);
1350
+ // ─── Comparison Operators ─────────────────────────────────────────────────
1351
+ case "greaterThan":
1352
+ return Number(cellValue) > Number(filterValue);
1353
+ case "lessThan":
1354
+ return Number(cellValue) < Number(filterValue);
1355
+ case "greaterOrEqual":
1356
+ return Number(cellValue) >= Number(filterValue);
1357
+ case "lessOrEqual":
1358
+ return Number(cellValue) <= Number(filterValue);
1359
+ // ─── Date Operators ───────────────────────────────────────────────────────
1360
+ case "before":
1361
+ if (cellValue == null) return false;
1362
+ return new Date(cellValue) < new Date(filterValue);
1363
+ case "after":
1364
+ if (cellValue == null) return false;
1365
+ return new Date(cellValue) > new Date(filterValue);
1366
+ case "between": {
1367
+ if (cellValue == null) return false;
1368
+ const [start, end] = filterValue;
1369
+ const date = new Date(cellValue);
1370
+ return date >= new Date(start) && date <= new Date(end);
1371
+ }
1372
+ // ─── Multi-Select Operators ───────────────────────────────────────────────
1373
+ case "hasAny": {
1374
+ const anyValues = filterValue;
1375
+ if (!Array.isArray(cellValue)) return false;
1376
+ return anyValues.some((v) => cellValue.includes(v));
1377
+ }
1378
+ case "hasAll": {
1379
+ const allValues = filterValue;
1380
+ if (!Array.isArray(cellValue)) return false;
1381
+ return allValues.every((v) => cellValue.includes(v));
1382
+ }
1383
+ case "hasNone": {
1384
+ const noneValues = filterValue;
1385
+ if (!Array.isArray(cellValue)) return true;
1386
+ return !noneValues.some((v) => cellValue.includes(v));
1387
+ }
1388
+ default:
1389
+ return true;
1390
+ }
1391
+ }
1392
+ function createEqualsFilter(columnId, value) {
1393
+ return {
1394
+ operator: "and",
1395
+ conditions: [{ columnId, operator: "equals", value }]
1396
+ };
1397
+ }
1398
+ function createAnyOfFilter(columnId, values) {
1399
+ return {
1400
+ operator: "or",
1401
+ conditions: values.map((value) => ({
1402
+ columnId,
1403
+ operator: "equals",
1404
+ value
1405
+ }))
1406
+ };
1407
+ }
1408
+ function combineFiltersAnd(filters) {
1409
+ return {
1410
+ operator: "and",
1411
+ conditions: filters
1412
+ };
1413
+ }
1414
+ function combineFiltersOr(filters) {
1415
+ return {
1416
+ operator: "or",
1417
+ conditions: filters
1418
+ };
1419
+ }
1420
+
1421
+ // src/database/form-types.ts
1422
+ var compact = (obj) => {
1423
+ const out = {};
1424
+ for (const [key, value] of Object.entries(obj)) {
1425
+ if (value !== void 0 && value !== "" && value !== false) out[key] = value;
1426
+ }
1427
+ return out;
1428
+ };
1429
+ function publicFormQuestion(question, column, resolvedOptions) {
1430
+ if (!column) return null;
1431
+ const type = column.type;
1432
+ if (!isFormFieldTypeAllowed(type, "public")) return null;
1433
+ const configOptions = column.config?.options;
1434
+ const options = resolvedOptions ?? configOptions ?? [];
1435
+ return compact({
1436
+ fieldId: question.fieldId,
1437
+ label: question.label,
1438
+ description: question.description,
1439
+ required: question.required,
1440
+ type,
1441
+ options: options.length > 0 ? options.map((o) => compact({ id: o.id, name: o.name, color: o.color })) : void 0
1442
+ });
1443
+ }
1444
+ function publishableRules(rules, asked) {
1445
+ const published = {};
1446
+ for (const [fieldId, rule] of Object.entries(rules ?? {})) {
1447
+ if (asked.has(fieldId) && rule.when.every((cond) => asked.has(cond.columnId))) {
1448
+ published[fieldId] = rule;
1449
+ }
1450
+ }
1451
+ return Object.keys(published).length > 0 ? published : void 0;
1452
+ }
1453
+ function buildPublicFormDefinition(config, rules, columns, fieldOptions) {
1454
+ const byId = new Map(columns.map((c) => [c.id, c]));
1455
+ const questions = config.questions.map((q) => publicFormQuestion(q, byId.get(q.fieldId), fieldOptions?.[q.fieldId])).filter((q) => q !== null);
1456
+ const asked = new Set(questions.map((q) => q.fieldId));
1457
+ return compact({
1458
+ title: config.title,
1459
+ description: config.description,
1460
+ questions,
1461
+ rules: publishableRules(rules, asked),
1462
+ submitLabel: config.submitLabel,
1463
+ confirmation: config.confirmation
1464
+ });
1465
+ }
1466
+ async function submissionRowId(tokenHash, nonce) {
1467
+ const bytes = new TextEncoder().encode(`form-submission\0${tokenHash}\0${nonce}`);
1468
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);
1469
+ const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
1470
+ return `formsub_${hex.slice(0, 24)}`;
1471
+ }
1472
+ var PUBLIC_SAFE_FORM_FIELD_TYPES = [
1473
+ "text",
1474
+ "number",
1475
+ "checkbox",
1476
+ "date",
1477
+ "dateRange",
1478
+ "select",
1479
+ "multiSelect",
1480
+ "url",
1481
+ "email",
1482
+ "phone"
1483
+ ];
1484
+ function isFormFieldTypeAllowed(type, audience) {
1485
+ if (isComputedFieldType(type) || isAutoFieldType(type)) return false;
1486
+ if (type === "richText") return false;
1487
+ if (audience === "public") {
1488
+ return PUBLIC_SAFE_FORM_FIELD_TYPES.includes(type);
1489
+ }
1490
+ return true;
1491
+ }
1492
+ function isFormQuestionVisible(rule, answers, columns) {
1493
+ if (!rule || rule.when.length === 0) return true;
1494
+ const pseudoRow = { id: "__form_draft__", cells: answers };
1495
+ const matched = filterRows([pseudoRow], columns, {
1496
+ operator: rule.match === "any" ? "or" : "and",
1497
+ conditions: rule.when
1498
+ });
1499
+ return matched.length === 1;
1500
+ }
1501
+ function visibleFormQuestions(config, rules, answers, columns, audience) {
1502
+ const byId = new Map(columns.map((c) => [c.id, c]));
1503
+ return config.questions.filter((q) => {
1504
+ const column = byId.get(q.fieldId);
1505
+ if (!column) return false;
1506
+ if (!isFormFieldTypeAllowed(column.type, audience)) return false;
1507
+ return isFormQuestionVisible(rules?.[q.fieldId], answers, columns);
1508
+ });
1509
+ }
1510
+ function isPlausibleValue(type, value) {
1511
+ if (value == null) return true;
1512
+ switch (type) {
1513
+ case "number":
1514
+ return typeof value === "number" && Number.isFinite(value);
1515
+ case "checkbox":
1516
+ return typeof value === "boolean";
1517
+ case "multiSelect":
1518
+ return Array.isArray(value);
1519
+ case "text":
1520
+ case "url":
1521
+ case "email":
1522
+ case "phone":
1523
+ case "select":
1524
+ case "date":
1525
+ return typeof value === "string";
1526
+ default:
1527
+ return true;
1528
+ }
1529
+ }
1530
+ function selectOptionsOf(column) {
1531
+ const config = column.config;
1532
+ if (!config?.options) return null;
1533
+ return new Set(config.options.map((o) => o.id));
1534
+ }
1535
+ var isEmptyAnswer = (value) => value == null || value === "" || Array.isArray(value) && value.length === 0;
1536
+ function answerDriftErrors(answers, configured, byId, audience) {
1537
+ const errors = [];
1538
+ for (const fieldId of Object.keys(answers)) {
1539
+ if (!configured.has(fieldId)) continue;
1540
+ const column = byId.get(fieldId);
1541
+ if (!column) {
1542
+ errors.push({ fieldId, reason: "unknown-field" });
1543
+ } else if (!isFormFieldTypeAllowed(column.type, audience)) {
1544
+ errors.push({ fieldId, reason: "type-not-allowed" });
1545
+ }
1546
+ }
1547
+ return errors;
1548
+ }
1549
+ function answerError(question, column, value) {
1550
+ if (isEmptyAnswer(value)) {
1551
+ return question.required ? "required" : void 0;
1552
+ }
1553
+ if (!isPlausibleValue(column.type, value)) return "bad-value";
1554
+ if (column.type === "select" || column.type === "multiSelect") {
1555
+ const known = selectOptionsOf(column);
1556
+ const ids = Array.isArray(value) ? value.map(String) : [String(value)];
1557
+ if (known && ids.some((id) => !known.has(id))) return "bad-option";
1558
+ }
1559
+ return void 0;
1560
+ }
1561
+ function validateFormSubmission(config, rules, answers, columns, audience) {
1562
+ const cells = {};
1563
+ const byId = new Map(columns.map((c) => [c.id, c]));
1564
+ const configured = new Set(config.questions.map((q) => q.fieldId));
1565
+ const visible = visibleFormQuestions(config, rules, answers, columns, audience);
1566
+ const errors = answerDriftErrors(answers, configured, byId, audience);
1567
+ for (const question of visible) {
1568
+ const column = byId.get(question.fieldId);
1569
+ if (!column) continue;
1570
+ const value = answers[question.fieldId] ?? null;
1571
+ const reason = answerError(question, column, value);
1572
+ if (reason) {
1573
+ errors.push({ fieldId: question.fieldId, reason });
1574
+ } else if (!isEmptyAnswer(value)) {
1575
+ cells[question.fieldId] = value;
1576
+ }
1577
+ }
1578
+ return { ok: errors.length === 0, errors, cells };
1579
+ }
1580
+
1289
1581
  // src/database/view-operations.ts
1290
1582
  import { nanoid as nanoid3 } from "nanoid";
1291
1583
  import * as Y4 from "yjs";
@@ -1535,136 +1827,6 @@ function operatorRequiresValue(operator) {
1535
1827
  return !["isEmpty", "isNotEmpty"].includes(operator);
1536
1828
  }
1537
1829
 
1538
- // src/database/filter-engine.ts
1539
- function filterRows(rows, columns, filter) {
1540
- if (!filter || filter.conditions.length === 0) {
1541
- return rows;
1542
- }
1543
- return rows.filter((row) => evaluateGroup(row, columns, filter));
1544
- }
1545
- function evaluateGroup(row, columns, group) {
1546
- if (group.conditions.length === 0) {
1547
- return true;
1548
- }
1549
- const results = group.conditions.map((condition) => {
1550
- if (isFilterGroup(condition)) {
1551
- return evaluateGroup(row, columns, condition);
1552
- }
1553
- return evaluateCondition(row, columns, condition);
1554
- });
1555
- if (group.operator === "and") {
1556
- return results.every(Boolean);
1557
- } else {
1558
- return results.some(Boolean);
1559
- }
1560
- }
1561
- function evaluateCondition(row, columns, condition) {
1562
- const column = columns.find((c) => c.id === condition.columnId);
1563
- if (!column) {
1564
- return true;
1565
- }
1566
- const cellValue = row.cells[condition.columnId];
1567
- const filterValue = condition.value;
1568
- return evaluateOperator(cellValue, filterValue, condition.operator, column.type);
1569
- }
1570
- function evaluateOperator(cellValue, filterValue, operator, columnType) {
1571
- switch (operator) {
1572
- // ─── Equality ─────────────────────────────────────────────────────────────
1573
- case "equals":
1574
- return cellValue === filterValue;
1575
- case "notEquals":
1576
- return cellValue !== filterValue;
1577
- // ─── Text Operators ───────────────────────────────────────────────────────
1578
- case "contains":
1579
- if (typeof cellValue === "string") {
1580
- return cellValue.toLowerCase().includes(String(filterValue).toLowerCase());
1581
- }
1582
- if (Array.isArray(cellValue)) {
1583
- return cellValue.includes(filterValue);
1584
- }
1585
- return false;
1586
- case "notContains":
1587
- return !evaluateOperator(cellValue, filterValue, "contains", columnType);
1588
- case "startsWith":
1589
- return typeof cellValue === "string" && cellValue.toLowerCase().startsWith(String(filterValue).toLowerCase());
1590
- case "endsWith":
1591
- return typeof cellValue === "string" && cellValue.toLowerCase().endsWith(String(filterValue).toLowerCase());
1592
- // ─── Empty Operators ──────────────────────────────────────────────────────
1593
- case "isEmpty":
1594
- return cellValue === null || cellValue === void 0 || cellValue === "" || Array.isArray(cellValue) && cellValue.length === 0;
1595
- case "isNotEmpty":
1596
- return !evaluateOperator(cellValue, null, "isEmpty", columnType);
1597
- // ─── Comparison Operators ─────────────────────────────────────────────────
1598
- case "greaterThan":
1599
- return Number(cellValue) > Number(filterValue);
1600
- case "lessThan":
1601
- return Number(cellValue) < Number(filterValue);
1602
- case "greaterOrEqual":
1603
- return Number(cellValue) >= Number(filterValue);
1604
- case "lessOrEqual":
1605
- return Number(cellValue) <= Number(filterValue);
1606
- // ─── Date Operators ───────────────────────────────────────────────────────
1607
- case "before":
1608
- if (cellValue == null) return false;
1609
- return new Date(cellValue) < new Date(filterValue);
1610
- case "after":
1611
- if (cellValue == null) return false;
1612
- return new Date(cellValue) > new Date(filterValue);
1613
- case "between": {
1614
- if (cellValue == null) return false;
1615
- const [start, end] = filterValue;
1616
- const date = new Date(cellValue);
1617
- return date >= new Date(start) && date <= new Date(end);
1618
- }
1619
- // ─── Multi-Select Operators ───────────────────────────────────────────────
1620
- case "hasAny": {
1621
- const anyValues = filterValue;
1622
- if (!Array.isArray(cellValue)) return false;
1623
- return anyValues.some((v) => cellValue.includes(v));
1624
- }
1625
- case "hasAll": {
1626
- const allValues = filterValue;
1627
- if (!Array.isArray(cellValue)) return false;
1628
- return allValues.every((v) => cellValue.includes(v));
1629
- }
1630
- case "hasNone": {
1631
- const noneValues = filterValue;
1632
- if (!Array.isArray(cellValue)) return true;
1633
- return !noneValues.some((v) => cellValue.includes(v));
1634
- }
1635
- default:
1636
- return true;
1637
- }
1638
- }
1639
- function createEqualsFilter(columnId, value) {
1640
- return {
1641
- operator: "and",
1642
- conditions: [{ columnId, operator: "equals", value }]
1643
- };
1644
- }
1645
- function createAnyOfFilter(columnId, values) {
1646
- return {
1647
- operator: "or",
1648
- conditions: values.map((value) => ({
1649
- columnId,
1650
- operator: "equals",
1651
- value
1652
- }))
1653
- };
1654
- }
1655
- function combineFiltersAnd(filters) {
1656
- return {
1657
- operator: "and",
1658
- conditions: filters
1659
- };
1660
- }
1661
- function combineFiltersOr(filters) {
1662
- return {
1663
- operator: "or",
1664
- conditions: filters
1665
- };
1666
- }
1667
-
1668
1830
  // src/database/sort-engine.ts
1669
1831
  function sortRows(rows, columns, sorts) {
1670
1832
  if (!sorts || sorts.length === 0) {
@@ -5410,6 +5572,18 @@ export {
5410
5572
  isFilterCondition,
5411
5573
  supportsGrouping,
5412
5574
  requiresDateColumn,
5575
+ filterRows,
5576
+ createEqualsFilter,
5577
+ createAnyOfFilter,
5578
+ combineFiltersAnd,
5579
+ combineFiltersOr,
5580
+ buildPublicFormDefinition,
5581
+ submissionRowId,
5582
+ PUBLIC_SAFE_FORM_FIELD_TYPES,
5583
+ isFormFieldTypeAllowed,
5584
+ isFormQuestionVisible,
5585
+ visibleFormQuestions,
5586
+ validateFormSubmission,
5413
5587
  getViews2,
5414
5588
  getView2,
5415
5589
  getViewByType,
@@ -5436,11 +5610,6 @@ export {
5436
5610
  isValidOperator,
5437
5611
  getOperatorLabel,
5438
5612
  operatorRequiresValue,
5439
- filterRows,
5440
- createEqualsFilter,
5441
- createAnyOfFilter,
5442
- combineFiltersAnd,
5443
- combineFiltersOr,
5444
5613
  sortRows,
5445
5614
  createSort,
5446
5615
  toggleSortDirection,