@nkzw/fate 0.0.7 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.mjs CHANGED
@@ -418,6 +418,12 @@ function isNodeItem(item) {
418
418
  function isNodesItem(item) {
419
419
  return "ids" in item;
420
420
  }
421
+ /** Indicates whether a request item represents a root query. */
422
+ function isQueryItem(item) {
423
+ return "view" in item && !("id" in item) && !("ids" in item);
424
+ }
425
+ /** Brand used on root definitions to mark their identity in the d.ts output. */
426
+ const RootKind = "__fate__root";
421
427
  /** Brand used on mutation definitions to mark their identity in the d.ts output. */
422
428
  const MutationKind = "__fate__mutation";
423
429
 
@@ -663,7 +669,7 @@ const emptySet$1 = /* @__PURE__ */ new Set();
663
669
  */
664
670
  function wrapMutation(client, identifier) {
665
671
  const config = client.getTypeConfig(identifier.entity);
666
- return async ({ args, delete: deleteRecord, input, optimistic, view: view$1 }) => {
672
+ return async ({ args, delete: deleteRecord, input, insert = "after", optimistic, view: view$1 }) => {
667
673
  const id$1 = maybeGetId(config.getId, input);
668
674
  const plan = view$1 ? getSelectionPlan(view$1, null) : void 0;
669
675
  const viewSelection = plan?.paths;
@@ -678,7 +684,7 @@ function wrapMutation(client, identifier) {
678
684
  const optimisticSelection = optimisticRecord ? collectImplicitSelectedPaths(optimisticRecord) : void 0;
679
685
  const selection = viewSelection || optimisticSelection ? new Set([...viewSelection ? [...viewSelection] : [], ...optimisticSelection ? [...optimisticSelection] : []]) : /* @__PURE__ */ new Set();
680
686
  const optimisticToken = optimisticEntityId ? client.registerOptimisticUpdate(optimisticEntityId, optimisticSelection ?? emptySet$1) : null;
681
- if (optimisticRecord && optimisticEntityId) client.write(identifier.entity, optimisticRecord, optimisticSelection ?? emptySet$1, snapshots, plan);
687
+ if (optimisticRecord && optimisticEntityId) client.write(identifier.entity, optimisticRecord, optimisticSelection ?? emptySet$1, snapshots, plan, null, null, insert);
682
688
  if (deleteRecord) {
683
689
  if (id$1 == null) throw new Error(`fate: Mutation '${identifier.key}' requires an 'id' to delete.`);
684
690
  client.deleteRecord(identifier.entity, id$1, snapshots, listSnapshots);
@@ -692,7 +698,7 @@ function wrapMutation(client, identifier) {
692
698
  const select = collectImplicitSelectedPaths(result);
693
699
  const pendingMask = optimisticEntityId ? client.getPendingOptimisticMask(optimisticEntityId, { excludeToken: optimisticToken }) : null;
694
700
  const filteredSelection = optimisticEntityId ? client.filterSelectionForPendingOptimistics(optimisticEntityId, select, { excludeToken: optimisticToken }) : select;
695
- client.write(identifier.entity, result, filteredSelection, void 0, plan, null, pendingMask);
701
+ client.write(identifier.entity, result, filteredSelection, void 0, plan, null, pendingMask, insert);
696
702
  if (deleteRecord && id$1 != null) client.deleteRecord(identifier.entity, id$1);
697
703
  const resultId = maybeGetId(config.getId, result);
698
704
  if (optimisticRecordId != null && resultId != null && optimisticRecordId !== resultId) client.deleteRecord(identifier.entity, optimisticRecordId);
@@ -1001,16 +1007,19 @@ const getRequestCacheKey = (request) => {
1001
1007
  for (const name of names) {
1002
1008
  const item = request[name];
1003
1009
  if (!item) continue;
1004
- const viewSignature = getViewSignature(item.root);
1005
1010
  if (isNodeItem(item)) {
1006
- parts.push(`node:${name}:${item.type}:${viewSignature}:${item.id}`);
1011
+ parts.push(`node:${name}:${getViewSignature(item.view)}:${item.id}`);
1007
1012
  continue;
1008
1013
  }
1009
1014
  if (isNodesItem(item)) {
1010
- parts.push(`node:${name}:${item.type}:${viewSignature}:${item.ids.map(serializeId).join(",")}`);
1015
+ parts.push(`node:${name}:${getViewSignature(item.view)}:${item.ids.map(serializeId).join(",")}`);
1016
+ continue;
1017
+ }
1018
+ if (isQueryItem(item)) {
1019
+ parts.push(`query:${name}:${getViewSignature(item.view)}:${item.args ? hashArgs(item.args) : ""}`);
1011
1020
  continue;
1012
1021
  }
1013
- parts.push(`list:${name}:${item.type}:${viewSignature}:${item.args ? hashArgs(item.args) : ""}`);
1022
+ parts.push(`list:${name}:${getViewSignature(item.list)}:${item.args ? hashArgs(item.args) : ""}`);
1014
1023
  }
1015
1024
  return parts.join("$");
1016
1025
  };
@@ -1038,22 +1047,25 @@ const groupSelectionByPrefix = (select) => {
1038
1047
  var FateClient = class {
1039
1048
  constructor(options) {
1040
1049
  this.parentLists = /* @__PURE__ */ new Map();
1050
+ this.rootLists = /* @__PURE__ */ new Map();
1041
1051
  this.pending = /* @__PURE__ */ new Map();
1042
1052
  this.optimisticMasks = /* @__PURE__ */ new Map();
1043
1053
  this.optimisticByEntity = /* @__PURE__ */ new Map();
1044
1054
  this.optimisticTokenCounter = 0;
1045
1055
  this.requests = /* @__PURE__ */ new Map();
1056
+ this.rootRequests = /* @__PURE__ */ new Map();
1046
1057
  this.stalledRequests = /* @__PURE__ */ new Set();
1047
1058
  this.store = new Store();
1048
1059
  this.viewDataCache = new ViewDataCache();
1060
+ this.actions = Object.create(null);
1061
+ this.mutationMap = Object.create(null);
1062
+ this.mutations = Object.create(null);
1063
+ this.roots = options.roots;
1049
1064
  this.transport = options.transport;
1050
1065
  this.types = new Map(options.types.map((entity) => [entity.type, {
1051
1066
  getId,
1052
1067
  ...entity
1053
1068
  }]));
1054
- this.mutationMap = Object.create(null);
1055
- this.mutations = Object.create(null);
1056
- this.actions = Object.create(null);
1057
1069
  if (options.mutations) for (const [key, definition] of Object.entries(options.mutations)) {
1058
1070
  const mutation$1 = wrapMutation(this, {
1059
1071
  ...definition,
@@ -1092,6 +1104,30 @@ var FateClient = class {
1092
1104
  }
1093
1105
  }
1094
1106
  }
1107
+ registerRootList(type, key) {
1108
+ let lists = this.rootLists.get(type);
1109
+ if (!lists) {
1110
+ lists = /* @__PURE__ */ new Set();
1111
+ this.rootLists.set(type, lists);
1112
+ }
1113
+ lists.add(key);
1114
+ }
1115
+ insertIntoRootLists(type, entityId, insert) {
1116
+ if (insert === "none") return;
1117
+ const listNames = this.rootLists.get(type);
1118
+ if (!listNames) return;
1119
+ for (const key of listNames) {
1120
+ const listState = this.store.getListState(key);
1121
+ if (!listState || listState.ids.includes(entityId)) continue;
1122
+ const ids = insert === "before" ? [entityId, ...listState.ids] : [...listState.ids, entityId];
1123
+ const cursors = listState.cursors ? insert === "before" ? [void 0, ...listState.cursors] : [...listState.cursors, void 0] : listState.cursors;
1124
+ this.store.setList(key, {
1125
+ cursors,
1126
+ ids,
1127
+ pagination: listState.pagination
1128
+ });
1129
+ }
1130
+ }
1095
1131
  getTypeConfig(type) {
1096
1132
  const config = this.types.get(type);
1097
1133
  if (!config) throw new Error(`fate: Unknown entity type '${type}'.`);
@@ -1108,8 +1144,8 @@ var FateClient = class {
1108
1144
  } : argsPayload ? { args: argsPayload } : input;
1109
1145
  return await this.transport.mutate(key, requestInput, select);
1110
1146
  }
1111
- write(type, data, select, snapshots, plan, pathPrefix = null, blockedMask) {
1112
- return this.writeEntity(type, data, select, snapshots, plan, pathPrefix, blockedMask);
1147
+ write(type, data, select, snapshots, plan, pathPrefix = null, blockedMask, insert) {
1148
+ return this.writeEntity(type, data, select, snapshots, plan, pathPrefix, blockedMask, insert);
1113
1149
  }
1114
1150
  deleteRecord(type, id$1, snapshots, listSnapshots) {
1115
1151
  const entityId = toEntityId(type, id$1);
@@ -1278,13 +1314,13 @@ var FateClient = class {
1278
1314
  ...connection.args,
1279
1315
  ...args
1280
1316
  };
1281
- const { argsPayload: argsPayload$1, plan: plan$1 } = this.resolveListSelection(view$1, requestArgs$1);
1317
+ const { argsPayload: argsPayload$1, plan: plan$1 } = this.resolveSelection(view$1, requestArgs$1);
1282
1318
  const { items, pagination } = await this.transport.fetchList(connection.field, plan$1.paths, argsPayload$1);
1283
1319
  if (!items) return this.store.getListState(connection.key);
1284
1320
  const incomingIds$1 = [];
1285
1321
  const incomingCursors$1 = [];
1286
1322
  for (const entry of items) {
1287
- const id$1 = this.write(connection.type, entry.node, plan$1.paths, void 0, plan$1);
1323
+ const id$1 = this.write(connection.type, entry.node, plan$1.paths, void 0, plan$1, null, null, "after");
1288
1324
  incomingIds$1.push(id$1);
1289
1325
  incomingCursors$1.push(entry.cursor);
1290
1326
  }
@@ -1296,6 +1332,7 @@ var FateClient = class {
1296
1332
  direction: isBackward ? "backward" : "forward",
1297
1333
  hasCursorArg
1298
1334
  });
1335
+ this.registerRootList(connection.type, connection.key);
1299
1336
  this.store.setList(connection.key, nextListState$1);
1300
1337
  return nextListState$1;
1301
1338
  }
@@ -1305,7 +1342,7 @@ var FateClient = class {
1305
1342
  ...args
1306
1343
  };
1307
1344
  if (requestArgs.id === void 0 && owner.id) requestArgs.id = owner.id;
1308
- const { argsPayload, plan } = this.resolveListSelection(view$1, requestArgs);
1345
+ const { argsPayload, plan } = this.resolveSelection(view$1, requestArgs);
1309
1346
  const nodeSelection = plan.paths;
1310
1347
  const scopedArgsPayload = argsPayload ? scopeArgsPayload(argsPayload, connection.field) : void 0;
1311
1348
  const parentSelection = /* @__PURE__ */ new Set();
@@ -1389,33 +1426,42 @@ var FateClient = class {
1389
1426
  this.executeRequest(request, { fetchAll: true }).catch(() => {});
1390
1427
  return result;
1391
1428
  }
1429
+ getRootType(name) {
1430
+ const root = this.roots[name];
1431
+ if (!root) throw new Error(`fate: Unknown root request '${name}'.`);
1432
+ return root.type;
1433
+ }
1392
1434
  async executeRequest(request, options = {}) {
1393
1435
  const fetchAll = options.fetchAll ?? false;
1394
1436
  const groups = /* @__PURE__ */ new Map();
1395
1437
  const promises = [];
1396
1438
  for (const [name, item] of Object.entries(request)) {
1439
+ const type = this.getRootType(name);
1397
1440
  const isNode = isNodeItem(item);
1398
1441
  if (isNode || isNodesItem(item)) {
1399
- const plan = getSelectionPlan(item.root, null);
1442
+ const plan = getSelectionPlan(item.view, null);
1400
1443
  const fields = plan.paths;
1401
- const fieldsSignature = [...fields].slice().sort().join(",");
1402
- const argsSignature = [...plan.args.entries()].map(([path, entry]) => `${path}:${entry.hash}`).sort().join(",");
1403
- const groupKey = `${item.type}#${fieldsSignature}|${argsSignature}`;
1444
+ const groupKey = `${type}#${[...fields].slice().sort().join(",")}|${[...plan.args.entries()].map(([path, entry]) => `${path}:${entry.hash}`).sort().join(",")}`;
1404
1445
  let group = groups.get(groupKey);
1405
1446
  if (!group) {
1406
1447
  group = {
1407
1448
  fields,
1408
1449
  ids: [],
1409
1450
  plan,
1410
- type: item.type
1451
+ type
1411
1452
  };
1412
1453
  groups.set(groupKey, group);
1413
1454
  }
1414
1455
  for (const raw of isNode ? [item.id] : item.ids) {
1415
- const entityId = toEntityId(item.type, raw);
1456
+ const entityId = toEntityId(type, raw);
1416
1457
  const missing = this.store.missingForSelection(entityId, fields);
1417
1458
  if (fetchAll || missing.size > 0) group.ids.push(raw);
1418
1459
  }
1460
+ } else if (isQueryItem(item)) {
1461
+ const { argsPayload, plan } = this.resolveSelection(item.view, item.args);
1462
+ const entityId = this.rootRequests.get(name);
1463
+ const missing = entityId ? this.store.missingForSelection(entityId, plan.paths) : plan.paths;
1464
+ if (fetchAll || missing.size > 0) promises.push(this.fetchQueryAndNormalize(name, item, plan, argsPayload));
1419
1465
  } else promises.push(this.fetchListAndNormalize(name, item));
1420
1466
  }
1421
1467
  await Promise.all([...promises, ...Array.from(groups.values()).map((group) => group.ids.length ? this.fetchByIdAndNormalize(group.type, group.ids, group.fields, group.plan) : Promise.resolve())]);
@@ -1423,14 +1469,21 @@ var FateClient = class {
1423
1469
  hasRequestData(request) {
1424
1470
  for (const [name, item] of Object.entries(request)) {
1425
1471
  const isNode = isNodeItem(item);
1472
+ const type = this.getRootType(name);
1426
1473
  if (isNode || isNodesItem(item)) {
1427
- const fields = getSelectionPlan(item.root, null).paths;
1474
+ const fields = getSelectionPlan(item.view, null).paths;
1428
1475
  for (const raw of isNode ? [item.id] : item.ids) {
1429
- const entityId = toEntityId(item.type, raw);
1476
+ const entityId = toEntityId(type, raw);
1430
1477
  if (this.store.missingForSelection(entityId, fields).size > 0) return false;
1431
1478
  }
1432
1479
  continue;
1433
1480
  }
1481
+ if (isQueryItem(item)) {
1482
+ const { plan } = this.resolveSelection(item.view, item.args);
1483
+ const entityId = this.rootRequests.get(name);
1484
+ if ((entityId ? this.store.missingForSelection(entityId, plan.paths) : plan.paths).size > 0) return false;
1485
+ continue;
1486
+ }
1434
1487
  if (!this.store.getList(name)) return false;
1435
1488
  }
1436
1489
  return true;
@@ -1438,18 +1491,28 @@ var FateClient = class {
1438
1491
  getRequestResult(request) {
1439
1492
  const result = {};
1440
1493
  for (const [name, item] of Object.entries(request)) {
1494
+ const type = this.getRootType(name);
1441
1495
  if (isNodeItem(item)) {
1442
- result[name] = this.ref(item.type, item.id, item.root);
1496
+ result[name] = this.ref(type, item.id, item.view);
1443
1497
  continue;
1444
1498
  }
1445
1499
  if (isNodesItem(item)) {
1446
- result[name] = item.ids.map((id$1) => this.ref(item.type, id$1, item.root));
1500
+ result[name] = item.ids.map((id$1) => this.ref(type, id$1, item.view));
1501
+ continue;
1502
+ }
1503
+ if (isQueryItem(item)) {
1504
+ const entityId = this.rootRequests.get(name);
1505
+ if (entityId) {
1506
+ const { id: id$1 } = parseEntityId(entityId);
1507
+ result[name] = createRef(type, id$1, item.view, { root: true });
1508
+ } else result[name] = null;
1447
1509
  continue;
1448
1510
  }
1449
1511
  const listState = this.store.getListState(name);
1450
- const nodeView = item.root && typeof item.root === "object" && "items" in item.root && item.root.items ? item.root.items.node ?? item.root : item.root;
1512
+ const hasItems = item.list && typeof item.list === "object" && "items" in item.list;
1513
+ const nodeView = hasItems && item.list.items ? item.list.items.node ?? item.list : item.list;
1451
1514
  const nodes = (listState?.ids ?? []).map((id$1) => this.rootListRef(id$1, nodeView));
1452
- if (item.root && typeof item.root === "object" && "items" in item.root) {
1515
+ if (hasItems) {
1453
1516
  const connection = {
1454
1517
  items: nodes.map((node, index) => ({
1455
1518
  cursor: listState?.cursors?.[index],
@@ -1457,7 +1520,7 @@ var FateClient = class {
1457
1520
  })),
1458
1521
  pagination: listState?.pagination
1459
1522
  };
1460
- const { argsPayload } = this.resolveListSelection(item.root, item.args);
1523
+ const { argsPayload } = this.resolveSelection(item.list, item.args);
1461
1524
  const metadata = {
1462
1525
  args: argsPayload,
1463
1526
  field: name,
@@ -1465,7 +1528,7 @@ var FateClient = class {
1465
1528
  owner: name,
1466
1529
  procedure: `request.${name}`,
1467
1530
  root: true,
1468
- type: item.type
1531
+ type
1469
1532
  };
1470
1533
  Object.defineProperty(connection, ConnectionTag, {
1471
1534
  configurable: false,
@@ -1485,24 +1548,33 @@ var FateClient = class {
1485
1548
  const records = await this.transport.fetchById(type, ids, select, resolvedArgs);
1486
1549
  for (const record of records) this.writeEntity(type, record, select, void 0, plan, prefix);
1487
1550
  }
1551
+ async fetchQueryAndNormalize(name, item, plan, argsPayload) {
1552
+ if (!this.transport.fetchQuery) throw new Error(`fate: transport does not support queries. Please add support for 'fetchQuery' in your transport for '${name}'.`);
1553
+ const record = await this.transport.fetchQuery(name, plan.paths, argsPayload);
1554
+ if (!record || typeof record !== "object") return;
1555
+ const entityId = this.writeEntity(this.getRootType(name), record, plan.paths, void 0, plan);
1556
+ this.rootRequests.set(name, entityId);
1557
+ }
1488
1558
  async fetchListAndNormalize(name, item) {
1489
1559
  if (!this.transport.fetchList) throw new Error(`fate: 'transport.fetchList' is not configured but request includes a list for key '${name}'.`);
1490
- const { argsPayload, plan } = this.resolveListSelection(item.root, item.args);
1560
+ const type = this.getRootType(name);
1561
+ const { argsPayload, plan } = this.resolveSelection(item.list, item.args);
1491
1562
  const { items, pagination } = await this.transport.fetchList(name, plan.paths, argsPayload);
1492
1563
  const ids = [];
1493
1564
  const cursors = [];
1494
1565
  for (const entry of items) {
1495
- const id$1 = this.writeEntity(item.type, entry.node, plan.paths, void 0, plan);
1566
+ const id$1 = this.writeEntity(type, entry.node, plan.paths, void 0, plan);
1496
1567
  ids.push(id$1);
1497
1568
  cursors.push(entry.cursor);
1498
1569
  }
1570
+ this.registerRootList(type, name);
1499
1571
  this.store.setList(name, {
1500
1572
  cursors,
1501
1573
  ids,
1502
1574
  pagination
1503
1575
  });
1504
1576
  }
1505
- resolveListSelection(view$1, args) {
1577
+ resolveSelection(view$1, args) {
1506
1578
  const plan = getSelectionPlan(view$1, null);
1507
1579
  const argsPayload = combineArgsPayload(args, resolvedArgsFromPlan(plan));
1508
1580
  if (argsPayload) applyArgsPayloadToPlan(plan, argsPayload);
@@ -1511,7 +1583,7 @@ var FateClient = class {
1511
1583
  plan
1512
1584
  };
1513
1585
  }
1514
- writeEntity(type, record, select, snapshots, plan, pathPrefix = null, blockedMask) {
1586
+ writeEntity(type, record, select, snapshots, plan, pathPrefix = null, blockedMask, insert) {
1515
1587
  const config = this.types.get(type);
1516
1588
  if (!config) throw new Error(`fate: Found unknown entity type '${type}' in normalization.`);
1517
1589
  const entityId = toEntityId(type, config.getId(record));
@@ -1528,6 +1600,10 @@ var FateClient = class {
1528
1600
  } else if (relationDescriptor && typeof relationDescriptor === "object" && "type" in relationDescriptor) {
1529
1601
  if (isFieldBlocked) continue;
1530
1602
  const childPaths = selectionTree.get(key) ?? emptySet;
1603
+ if (value === null) {
1604
+ result[key] = null;
1605
+ continue;
1606
+ }
1531
1607
  if (value && typeof value === "object" && !isNodeRef(value)) {
1532
1608
  const childType = relationDescriptor.type;
1533
1609
  const childConfig = this.types.get(childType);
@@ -1538,6 +1614,10 @@ var FateClient = class {
1538
1614
  } else if (relationDescriptor && typeof relationDescriptor === "object" && "listOf" in relationDescriptor) {
1539
1615
  if (isFieldBlocked) continue;
1540
1616
  const childPaths = selectionTree.get(key) ?? emptySet;
1617
+ if (value === null) {
1618
+ result[key] = null;
1619
+ continue;
1620
+ }
1541
1621
  const childType = relationDescriptor.listOf;
1542
1622
  const childConfig = this.types.get(childType);
1543
1623
  if (!childConfig) throw new Error(`fate: Unknown related type '${childType}' (field '${type}.${key}').`);
@@ -1614,10 +1694,12 @@ var FateClient = class {
1614
1694
  if (snapshots && !snapshots.has(entityId)) snapshots.set(entityId, this.store.snapshot(entityId));
1615
1695
  this.viewDataCache.invalidate(entityId);
1616
1696
  this.store.merge(entityId, result, select);
1617
- this.linkParentLists(type, entityId, result, snapshots);
1697
+ this.linkParentLists(type, entityId, result, snapshots, insert ?? "after");
1698
+ if (!pathPrefix && insert) this.insertIntoRootLists(type, entityId, insert);
1618
1699
  return entityId;
1619
1700
  }
1620
- linkParentLists(type, entityId, record, snapshots) {
1701
+ linkParentLists(type, entityId, record, snapshots, insert) {
1702
+ if (insert === "none") return;
1621
1703
  const parents = this.parentLists.get(type);
1622
1704
  if (!parents) return;
1623
1705
  for (const parent of parents) {
@@ -1631,11 +1713,11 @@ var FateClient = class {
1631
1713
  if (current.some((item) => isNodeRef(item) && getNodeRefId(item) === entityId)) continue;
1632
1714
  if (snapshots && !snapshots.has(parentId)) snapshots.set(parentId, this.store.snapshot(parentId));
1633
1715
  this.viewDataCache.invalidate(parentId);
1634
- const nextList = [...current, createNodeRef(entityId)];
1716
+ const nextList = insert === "before" ? [createNodeRef(entityId), ...current] : [...current, createNodeRef(entityId)];
1635
1717
  const ids = nextList.map((item) => isNodeRef(item) ? getNodeRefId(item) : null).filter((id$1) => id$1 != null);
1636
1718
  const defaultListKey = getListKey(parentId, parent.field);
1637
1719
  const defaultListState = this.store.getListState(defaultListKey);
1638
- const nextDefaultCursors = defaultListState?.cursors && ids.length > defaultListState.cursors.length ? [...defaultListState.cursors, ...new Array(ids.length - defaultListState.cursors.length).fill(void 0)] : defaultListState?.cursors;
1720
+ const nextDefaultCursors = defaultListState?.cursors ? insert === "before" ? [void 0, ...defaultListState.cursors] : [...defaultListState.cursors, void 0] : defaultListState?.cursors;
1639
1721
  this.store.setList(defaultListKey, {
1640
1722
  cursors: nextDefaultCursors,
1641
1723
  ids,
@@ -1644,8 +1726,8 @@ var FateClient = class {
1644
1726
  const registeredLists = this.store.getListsForField(parentId, parent.field).filter(([key]) => key !== defaultListKey);
1645
1727
  for (const [listKey, listState] of registeredLists) {
1646
1728
  if (listState.ids.includes(entityId)) continue;
1647
- const listIds = [...listState.ids, entityId];
1648
- const listCursors = listState.cursors ? [...listState.cursors, void 0] : void 0;
1729
+ const listIds = insert === "before" ? [entityId, ...listState.ids] : [...listState.ids, entityId];
1730
+ const listCursors = listState.cursors ? insert === "before" ? [void 0, ...listState.cursors] : [...listState.cursors, void 0] : void 0;
1649
1731
  this.store.setList(listKey, {
1650
1732
  cursors: listCursors,
1651
1733
  ids: listIds,
@@ -1681,6 +1763,10 @@ var FateClient = class {
1681
1763
  if (!(key in target)) target[key] = {};
1682
1764
  const nextSelection = Object.keys(selectionWithoutArgs).length ? selectionWithoutArgs : selectionValue;
1683
1765
  const value = record$1[key];
1766
+ if (value == null) {
1767
+ target[key] = null;
1768
+ continue;
1769
+ }
1684
1770
  if (Array.isArray(value)) if (nextSelection.items && typeof nextSelection.items === "object") {
1685
1771
  const selection = nextSelection.items;
1686
1772
  const fieldArgs = plan.args.get(fieldPath);
@@ -1806,7 +1892,7 @@ function createClient(options) {
1806
1892
  * Builds a `Transport` backed by a tRPC client using the configured resolvers
1807
1893
  * for by-id queries, lists, and mutations.
1808
1894
  */
1809
- function createTRPCTransport({ byId, client, lists, mutations }) {
1895
+ function createTRPCTransport({ byId, client, lists, mutations, queries }) {
1810
1896
  const transport = {
1811
1897
  async fetchById(type, ids, select, args) {
1812
1898
  const resolver = byId[type];
@@ -1825,6 +1911,15 @@ function createTRPCTransport({ byId, client, lists, mutations }) {
1825
1911
  args,
1826
1912
  select: [...select]
1827
1913
  });
1914
+ },
1915
+ async fetchQuery(procedure, select, args) {
1916
+ if (!queries) throw new Error(`fate(trpc): No query resolvers configured; cannot call "${procedure}".`);
1917
+ const resolver = queries[procedure];
1918
+ if (!resolver) throw new Error(`fate(trpc): Missing query resolver for procedure "${procedure}"`);
1919
+ return resolver(client)({
1920
+ args,
1921
+ select: [...select]
1922
+ });
1828
1923
  }
1829
1924
  };
1830
1925
  transport.mutate = async (procedure, input, select) => {
@@ -1839,4 +1934,16 @@ function createTRPCTransport({ byId, client, lists, mutations }) {
1839
1934
  }
1840
1935
 
1841
1936
  //#endregion
1842
- export { ConnectionTag, FateClient, createClient, createTRPCTransport, getSelectionPlan, isViewTag, mutation, toEntityId, view };
1937
+ //#region src/root.ts
1938
+ /**
1939
+ * Defines a root query for an entity type, capturing the response shape.
1940
+ */
1941
+ function clientRoot(type) {
1942
+ return Object.freeze({
1943
+ [RootKind]: true,
1944
+ type
1945
+ });
1946
+ }
1947
+
1948
+ //#endregion
1949
+ export { ConnectionTag, FateClient, clientRoot, createClient, createTRPCTransport, getSelectionPlan, isViewTag, mutation, toEntityId, view };
package/lib/server.d.mts CHANGED
@@ -1,30 +1,28 @@
1
+ import { t as AnyRecord } from "./types-CNKGKRtz.mjs";
1
2
  import { z } from "zod";
2
3
  import { TRPCProcedureBuilder } from "@trpc/server";
3
4
 
4
5
  //#region src/server/dataView.d.ts
5
6
  declare const dataViewFieldsKey: unique symbol;
6
- type AnyRecord = Record<string, unknown>;
7
7
  type ResolverSelect<Context> = AnyRecord | ((options: {
8
8
  args?: AnyRecord;
9
9
  context?: Context;
10
10
  }) => AnyRecord | void);
11
11
  type Bivariant<Fn extends (...args: Array<any>) => unknown> = {
12
- bivarianceHack(...args: Parameters<Fn>): ReturnType<Fn>;
13
- }['bivarianceHack'];
14
- type ResolverResolve<Item$1 extends AnyRecord, Context> = Bivariant<(options: {
15
- args?: AnyRecord;
16
- context?: Context;
17
- item: Item$1;
18
- }) => Promise<unknown> | unknown>;
12
+ bivariance(...args: Parameters<Fn>): ReturnType<Fn>;
13
+ }['bivariance'];
14
+ type ResolverResolve<Item$1 extends AnyRecord, Result, Context> = Bivariant<(item: Item$1, context?: Context, args?: AnyRecord) => Promise<Result> | Result>;
15
+ type ResolverAuthorize<Item$1 extends AnyRecord, Context> = Bivariant<(item: Item$1, context?: Context, args?: AnyRecord) => Promise<boolean> | boolean>;
19
16
  /**
20
17
  * Field configuration for selecting and resolving a computed value on the backend.
21
18
  */
22
- type ResolverField<Item$1 extends AnyRecord, Context> = {
19
+ type ResolverField<Item$1 extends AnyRecord, Result, Context> = {
20
+ authorize?: ResolverAuthorize<Item$1, Context>;
23
21
  kind: 'resolver';
24
- resolve: ResolverResolve<Item$1, Context>;
22
+ resolve: ResolverResolve<Item$1, Result, Context>;
25
23
  select?: ResolverSelect<Context>;
26
24
  };
27
- type DataField<Item$1 extends AnyRecord, Context> = true | DataView<AnyRecord, Context> | ResolverField<Item$1, Context>;
25
+ type DataField<Item$1 extends AnyRecord> = true | DataView<AnyRecord> | ResolverField<Item$1, any, any>;
28
26
  /**
29
27
  * Recursively serializes resolver results for transport across the network.
30
28
  */
@@ -33,15 +31,15 @@ type Serializable<T> = T extends Date ? string : T extends Array<infer U> ? Arra
33
31
  * Server-side mirror of a view definition describing how to select and resolve
34
32
  * fields when fulfilling a client request.
35
33
  */
36
- type DataView<Item$1 extends AnyRecord, Context = unknown> = {
37
- fields: Record<string, DataField<Item$1, Context>>;
34
+ type DataView<Item$1 extends AnyRecord> = {
35
+ fields: Record<string, DataField<Item$1>>;
38
36
  kind?: 'resolver' | 'list';
39
37
  typeName: string;
40
38
  };
41
39
  /**
42
40
  * Convenience type for declaring the fields of a server data view.
43
41
  */
44
- type DataViewConfig<Item$1 extends AnyRecord, Context> = Record<string, DataField<Item$1, Context>>;
42
+ type DataViewConfig<Item$1 extends AnyRecord> = Record<string, DataField<Item$1>>;
45
43
  /**
46
44
  * Declares a server data view that exposes an object's available fields to the client.
47
45
  *
@@ -51,43 +49,44 @@ type DataViewConfig<Item$1 extends AnyRecord, Context> = Record<string, DataFiel
51
49
  * title: true,
52
50
  * });
53
51
  */
54
- declare function dataView<Item$1 extends AnyRecord, Context = unknown>(typeName: string): <Fields extends DataViewConfig<Item$1, Context>>(fields: Fields) => DataView<Item$1, Context> & {
52
+ declare function dataView<Item$1 extends AnyRecord>(typeName: string): <Fields extends DataViewConfig<Item$1>>(fields: Fields) => DataView<Item$1> & {
55
53
  readonly [dataViewFieldsKey]: Fields;
56
54
  };
57
55
  /**
58
56
  * Marks a data view as a list resolver so the server can respond with
59
57
  * connection information.
60
58
  */
61
- declare const list: <Item$1 extends AnyRecord, Context>(view: DataView<Item$1, Context>) => {
59
+ declare const list: <Item$1 extends AnyRecord>(view: DataView<Item$1>) => {
62
60
  kind: "list";
63
- fields: Record<string, DataField<Item$1, Context>>;
61
+ fields: Record<string, DataField<Item$1>>;
64
62
  typeName: string;
65
63
  };
66
64
  /**
67
65
  * Declares a resolver field inside a data view, optionally providing a
68
66
  * selection for any data dependencies.
69
67
  */
70
- declare function resolver<Item$1 extends AnyRecord, Context = unknown>(config: {
71
- resolve: ResolverResolve<Item$1, Context>;
68
+ declare function resolver<Item$1 extends AnyRecord, Result = unknown, Context = unknown>(config: {
69
+ authorize?: ResolverAuthorize<Item$1, Context>;
70
+ resolve: ResolverResolve<Item$1, Result, Context>;
72
71
  select?: ResolverSelect<Context>;
73
- }): ResolverField<Item$1, Context>;
72
+ }): ResolverField<Item$1, Result, Context>;
74
73
  type NonNullish<T> = Exclude<T, null | undefined>;
75
74
  type WithNullish<Original, Value> = null extends Original ? undefined extends Original ? Value | null | undefined : Value | null : undefined extends Original ? Value | undefined : Value;
76
- type ResolverResult<Field> = Field extends ResolverField<AnyRecord, unknown> ? Awaited<ReturnType<Field['resolve']>> : never;
77
- type RelationResult<ItemField, V extends DataView<AnyRecord, unknown>> = NonNullish<ItemField> extends Array<unknown> ? WithNullish<ItemField, Array<RawDataViewResult<V>>> : WithNullish<ItemField, RawDataViewResult<V>>;
78
- type ViewFieldConfig<V extends DataView<AnyRecord, unknown>> = V extends {
75
+ type ResolverResult<Field> = Field extends ResolverField<AnyRecord, any, any> ? Field['authorize'] extends ResolverAuthorize<any, any> ? Awaited<ReturnType<Field['resolve']>> | null : Awaited<ReturnType<Field['resolve']>> : never;
76
+ type RelationResult<ItemField, V extends DataView<AnyRecord>> = NonNullish<ItemField> extends Array<unknown> ? WithNullish<ItemField, Array<RawDataViewResult<V>>> : WithNullish<ItemField, RawDataViewResult<V>>;
77
+ type ViewFieldConfig<V extends DataView<AnyRecord>> = V extends {
79
78
  readonly [dataViewFieldsKey]: infer Fields;
80
79
  } ? Fields : V['fields'];
81
- type RawFieldResult<Item$1 extends AnyRecord, Key extends PropertyKey, Field extends DataField<Item$1, unknown>> = Field extends true ? Key extends keyof Item$1 ? Item$1[Key] : never : Field extends DataView<infer ChildItem, unknown> ? Key extends keyof Item$1 ? RelationResult<Item$1[Key], DataView<ChildItem, unknown>> : never : Field extends ResolverField<Item$1, unknown> ? ResolverResult<Field> : never;
82
- type RawDataViewResult<V extends DataView<AnyRecord, unknown>> = V extends DataView<infer Item, unknown> ? { [K in keyof ViewFieldConfig<V>]: RawFieldResult<Item, K, ViewFieldConfig<V>[K]> } : never;
83
- type DataViewResult<V extends DataView<AnyRecord, unknown>> = Serializable<RawDataViewResult<V>>;
80
+ type RawFieldResult<Item$1 extends AnyRecord, Key extends PropertyKey, Field extends DataField<Item$1>> = Field extends true ? Key extends keyof Item$1 ? Item$1[Key] : never : Field extends DataView<infer ChildItem> ? Key extends keyof Item$1 ? RelationResult<Item$1[Key], DataView<ChildItem>> : never : Field extends ResolverField<Item$1, any, any> ? ResolverResult<Field> : never;
81
+ type RawDataViewResult<V extends DataView<AnyRecord>> = V extends DataView<infer Item> ? { [K in keyof ViewFieldConfig<V>]: RawFieldResult<Item, K, ViewFieldConfig<V>[K]> } : never;
82
+ type DataViewResult<V extends DataView<AnyRecord>> = Serializable<RawDataViewResult<V>>;
84
83
  type WithTypename<T, Name extends string> = T & {
85
84
  __typename: Name;
86
85
  };
87
86
  /**
88
87
  * Resolved entity type from a data view for client use.
89
88
  */
90
- type Entity<TView extends DataView<AnyRecord, unknown>, Name extends string, Replacements extends Record<string, unknown> = Record<string, never>> = WithTypename<Omit<DataViewResult<TView>, keyof Replacements> & Replacements, Name>;
89
+ type Entity<TView extends DataView<AnyRecord>, Name extends string, Replacements extends Record<string, unknown> = Record<never, never>> = WithTypename<Omit<DataViewResult<TView>, keyof Replacements> & Replacements, Name>;
91
90
  /**
92
91
  * Builds a resolver that applies a client's selection to a server data view,
93
92
  * filtering fields, running nested resolvers, and shaping selects.
@@ -101,7 +100,7 @@ declare function createResolver<Item$1 extends AnyRecord, Context = unknown>({
101
100
  args?: AnyRecord;
102
101
  ctx?: Context;
103
102
  select: Iterable<string>;
104
- view: DataView<Item$1, Context>;
103
+ view: DataView<Item$1>;
105
104
  }): {
106
105
  resolve: (item: Item$1) => Promise<AnyRecord>;
107
106
  resolveMany: (items: Array<AnyRecord>) => Promise<Array<AnyRecord>>;
@@ -164,4 +163,11 @@ declare const withConnection: <TContext>(procedure: ProcedureLike<TContext>) =>
164
163
  query: QueryFn<TContext, TItem, ConnectionInputWithAdditional<TAdditionalInput>>;
165
164
  }) => ReturnType<ReturnType<ProcedureLike<TContext>["input"]>["query"]>;
166
165
  //#endregion
167
- export { type Entity, connectionArgs, createResolver, dataView, list, resolver, withConnection };
166
+ //#region src/server/input.d.ts
167
+ declare const byIdInput: z.ZodObject<{
168
+ args: z.ZodOptional<z.ZodType<Record<string, unknown>, unknown, z.core.$ZodTypeInternals<Record<string, unknown>, unknown>>>;
169
+ ids: z.ZodArray<z.ZodString>;
170
+ select: z.ZodArray<z.ZodString>;
171
+ }, z.core.$strip>;
172
+ //#endregion
173
+ export { type Entity, byIdInput, connectionArgs, createResolver, dataView, list, resolver, withConnection };
package/lib/server.mjs CHANGED
@@ -374,24 +374,26 @@ const collectResolvers = (node, select, args$1, context) => {
374
374
  }
375
375
  for (const relation of node.relations.values()) collectResolvers(relation, select, args$1, context);
376
376
  };
377
- const resolveNode = async (options) => {
378
- const { item, node, options: resolverOptions } = options;
377
+ const resolveNode = async ({ item, node, options: resolverOptions }) => {
379
378
  if (!isRecord(item)) return item;
380
379
  let result = null;
381
380
  const assign = (key, value) => {
382
381
  if (!result) result = { ...item };
383
382
  result[key] = value;
384
383
  };
385
- const base = () => result ?? item;
384
+ const getItem = () => result ?? item;
386
385
  for (const [field, resolver$1] of node.resolvers) {
387
- const value = await resolver$1.resolve({
388
- ...resolverOptions,
389
- item: base()
390
- });
386
+ if (resolver$1.authorize) {
387
+ if (!await resolver$1.authorize(getItem(), resolverOptions.context, resolverOptions.args)) {
388
+ assign(field, null);
389
+ continue;
390
+ }
391
+ }
392
+ const value = await resolver$1.resolve(getItem(), resolverOptions.context, resolverOptions.args);
391
393
  if (value !== void 0) assign(field, value);
392
394
  }
393
395
  for (const [field, relationNode] of node.relations) {
394
- const current = base()[field];
396
+ const current = getItem()[field];
395
397
  if (Array.isArray(current)) {
396
398
  const resolved = await Promise.all(current.map((entry) => resolveNode({
397
399
  item: entry,
@@ -447,4 +449,12 @@ function createResolver({ args: args$1, ctx, select: initialSelect, view }) {
447
449
  }
448
450
 
449
451
  //#endregion
450
- export { connectionArgs, createResolver, dataView, list, resolver, withConnection };
452
+ //#region src/server/input.ts
453
+ const byIdInput = z.object({
454
+ args: connectionArgs,
455
+ ids: z.array(z.string().min(1)).nonempty(),
456
+ select: z.array(z.string())
457
+ });
458
+
459
+ //#endregion
460
+ export { byIdInput, connectionArgs, createResolver, dataView, list, resolver, withConnection };