@nkzw/fate 0.0.6 → 0.0.8

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,10 @@ 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
+ }
421
425
  /** Brand used on mutation definitions to mark their identity in the d.ts output. */
422
426
  const MutationKind = "__fate__mutation";
423
427
 
@@ -663,7 +667,7 @@ const emptySet$1 = /* @__PURE__ */ new Set();
663
667
  */
664
668
  function wrapMutation(client, identifier) {
665
669
  const config = client.getTypeConfig(identifier.entity);
666
- return async ({ args, delete: deleteRecord, input, optimistic, view: view$1 }) => {
670
+ return async ({ args, delete: deleteRecord, input, insert = "after", optimistic, view: view$1 }) => {
667
671
  const id$1 = maybeGetId(config.getId, input);
668
672
  const plan = view$1 ? getSelectionPlan(view$1, null) : void 0;
669
673
  const viewSelection = plan?.paths;
@@ -678,7 +682,7 @@ function wrapMutation(client, identifier) {
678
682
  const optimisticSelection = optimisticRecord ? collectImplicitSelectedPaths(optimisticRecord) : void 0;
679
683
  const selection = viewSelection || optimisticSelection ? new Set([...viewSelection ? [...viewSelection] : [], ...optimisticSelection ? [...optimisticSelection] : []]) : /* @__PURE__ */ new Set();
680
684
  const optimisticToken = optimisticEntityId ? client.registerOptimisticUpdate(optimisticEntityId, optimisticSelection ?? emptySet$1) : null;
681
- if (optimisticRecord && optimisticEntityId) client.write(identifier.entity, optimisticRecord, optimisticSelection ?? emptySet$1, snapshots, plan);
685
+ if (optimisticRecord && optimisticEntityId) client.write(identifier.entity, optimisticRecord, optimisticSelection ?? emptySet$1, snapshots, plan, null, null, insert);
682
686
  if (deleteRecord) {
683
687
  if (id$1 == null) throw new Error(`fate: Mutation '${identifier.key}' requires an 'id' to delete.`);
684
688
  client.deleteRecord(identifier.entity, id$1, snapshots, listSnapshots);
@@ -692,7 +696,7 @@ function wrapMutation(client, identifier) {
692
696
  const select = collectImplicitSelectedPaths(result);
693
697
  const pendingMask = optimisticEntityId ? client.getPendingOptimisticMask(optimisticEntityId, { excludeToken: optimisticToken }) : null;
694
698
  const filteredSelection = optimisticEntityId ? client.filterSelectionForPendingOptimistics(optimisticEntityId, select, { excludeToken: optimisticToken }) : select;
695
- client.write(identifier.entity, result, filteredSelection, void 0, plan, null, pendingMask);
699
+ client.write(identifier.entity, result, filteredSelection, void 0, plan, null, pendingMask, insert);
696
700
  if (deleteRecord && id$1 != null) client.deleteRecord(identifier.entity, id$1);
697
701
  const resultId = maybeGetId(config.getId, result);
698
702
  if (optimisticRecordId != null && resultId != null && optimisticRecordId !== resultId) client.deleteRecord(identifier.entity, optimisticRecordId);
@@ -1001,16 +1005,19 @@ const getRequestCacheKey = (request) => {
1001
1005
  for (const name of names) {
1002
1006
  const item = request[name];
1003
1007
  if (!item) continue;
1004
- const viewSignature = getViewSignature(item.root);
1005
1008
  if (isNodeItem(item)) {
1006
- parts.push(`node:${name}:${item.type}:${viewSignature}:${item.id}`);
1009
+ parts.push(`node:${name}:${item.type}:${getViewSignature(item.view)}:${item.id}`);
1007
1010
  continue;
1008
1011
  }
1009
1012
  if (isNodesItem(item)) {
1010
- parts.push(`node:${name}:${item.type}:${viewSignature}:${item.ids.map(serializeId).join(",")}`);
1013
+ parts.push(`node:${name}:${item.type}:${getViewSignature(item.view)}:${item.ids.map(serializeId).join(",")}`);
1014
+ continue;
1015
+ }
1016
+ if (isQueryItem(item)) {
1017
+ parts.push(`query:${name}:${item.type}:${getViewSignature(item.view)}:${item.args ? hashArgs(item.args) : ""}`);
1011
1018
  continue;
1012
1019
  }
1013
- parts.push(`list:${name}:${item.type}:${viewSignature}:${item.args ? hashArgs(item.args) : ""}`);
1020
+ parts.push(`list:${name}:${item.type}:${getViewSignature(item.list)}:${item.args ? hashArgs(item.args) : ""}`);
1014
1021
  }
1015
1022
  return parts.join("$");
1016
1023
  };
@@ -1038,11 +1045,13 @@ const groupSelectionByPrefix = (select) => {
1038
1045
  var FateClient = class {
1039
1046
  constructor(options) {
1040
1047
  this.parentLists = /* @__PURE__ */ new Map();
1048
+ this.rootLists = /* @__PURE__ */ new Map();
1041
1049
  this.pending = /* @__PURE__ */ new Map();
1042
1050
  this.optimisticMasks = /* @__PURE__ */ new Map();
1043
1051
  this.optimisticByEntity = /* @__PURE__ */ new Map();
1044
1052
  this.optimisticTokenCounter = 0;
1045
1053
  this.requests = /* @__PURE__ */ new Map();
1054
+ this.rootRequests = /* @__PURE__ */ new Map();
1046
1055
  this.stalledRequests = /* @__PURE__ */ new Set();
1047
1056
  this.store = new Store();
1048
1057
  this.viewDataCache = new ViewDataCache();
@@ -1092,6 +1101,30 @@ var FateClient = class {
1092
1101
  }
1093
1102
  }
1094
1103
  }
1104
+ registerRootList(type, key) {
1105
+ let lists = this.rootLists.get(type);
1106
+ if (!lists) {
1107
+ lists = /* @__PURE__ */ new Set();
1108
+ this.rootLists.set(type, lists);
1109
+ }
1110
+ lists.add(key);
1111
+ }
1112
+ insertIntoRootLists(type, entityId, insert) {
1113
+ if (insert === "none") return;
1114
+ const listNames = this.rootLists.get(type);
1115
+ if (!listNames) return;
1116
+ for (const key of listNames) {
1117
+ const listState = this.store.getListState(key);
1118
+ if (!listState || listState.ids.includes(entityId)) continue;
1119
+ const ids = insert === "before" ? [entityId, ...listState.ids] : [...listState.ids, entityId];
1120
+ const cursors = listState.cursors ? insert === "before" ? [void 0, ...listState.cursors] : [...listState.cursors, void 0] : listState.cursors;
1121
+ this.store.setList(key, {
1122
+ cursors,
1123
+ ids,
1124
+ pagination: listState.pagination
1125
+ });
1126
+ }
1127
+ }
1095
1128
  getTypeConfig(type) {
1096
1129
  const config = this.types.get(type);
1097
1130
  if (!config) throw new Error(`fate: Unknown entity type '${type}'.`);
@@ -1108,8 +1141,8 @@ var FateClient = class {
1108
1141
  } : argsPayload ? { args: argsPayload } : input;
1109
1142
  return await this.transport.mutate(key, requestInput, select);
1110
1143
  }
1111
- write(type, data, select, snapshots, plan, pathPrefix = null, blockedMask) {
1112
- return this.writeEntity(type, data, select, snapshots, plan, pathPrefix, blockedMask);
1144
+ write(type, data, select, snapshots, plan, pathPrefix = null, blockedMask, insert) {
1145
+ return this.writeEntity(type, data, select, snapshots, plan, pathPrefix, blockedMask, insert);
1113
1146
  }
1114
1147
  deleteRecord(type, id$1, snapshots, listSnapshots) {
1115
1148
  const entityId = toEntityId(type, id$1);
@@ -1278,13 +1311,13 @@ var FateClient = class {
1278
1311
  ...connection.args,
1279
1312
  ...args
1280
1313
  };
1281
- const { argsPayload: argsPayload$1, plan: plan$1 } = this.resolveListSelection(view$1, requestArgs$1);
1314
+ const { argsPayload: argsPayload$1, plan: plan$1 } = this.resolveSelection(view$1, requestArgs$1);
1282
1315
  const { items, pagination } = await this.transport.fetchList(connection.field, plan$1.paths, argsPayload$1);
1283
1316
  if (!items) return this.store.getListState(connection.key);
1284
1317
  const incomingIds$1 = [];
1285
1318
  const incomingCursors$1 = [];
1286
1319
  for (const entry of items) {
1287
- const id$1 = this.write(connection.type, entry.node, plan$1.paths, void 0, plan$1);
1320
+ const id$1 = this.write(connection.type, entry.node, plan$1.paths, void 0, plan$1, null, null, "after");
1288
1321
  incomingIds$1.push(id$1);
1289
1322
  incomingCursors$1.push(entry.cursor);
1290
1323
  }
@@ -1296,6 +1329,7 @@ var FateClient = class {
1296
1329
  direction: isBackward ? "backward" : "forward",
1297
1330
  hasCursorArg
1298
1331
  });
1332
+ this.registerRootList(connection.type, connection.key);
1299
1333
  this.store.setList(connection.key, nextListState$1);
1300
1334
  return nextListState$1;
1301
1335
  }
@@ -1305,7 +1339,7 @@ var FateClient = class {
1305
1339
  ...args
1306
1340
  };
1307
1341
  if (requestArgs.id === void 0 && owner.id) requestArgs.id = owner.id;
1308
- const { argsPayload, plan } = this.resolveListSelection(view$1, requestArgs);
1342
+ const { argsPayload, plan } = this.resolveSelection(view$1, requestArgs);
1309
1343
  const nodeSelection = plan.paths;
1310
1344
  const scopedArgsPayload = argsPayload ? scopeArgsPayload(argsPayload, connection.field) : void 0;
1311
1345
  const parentSelection = /* @__PURE__ */ new Set();
@@ -1396,7 +1430,7 @@ var FateClient = class {
1396
1430
  for (const [name, item] of Object.entries(request)) {
1397
1431
  const isNode = isNodeItem(item);
1398
1432
  if (isNode || isNodesItem(item)) {
1399
- const plan = getSelectionPlan(item.root, null);
1433
+ const plan = getSelectionPlan(item.view, null);
1400
1434
  const fields = plan.paths;
1401
1435
  const fieldsSignature = [...fields].slice().sort().join(",");
1402
1436
  const argsSignature = [...plan.args.entries()].map(([path, entry]) => `${path}:${entry.hash}`).sort().join(",");
@@ -1416,6 +1450,11 @@ var FateClient = class {
1416
1450
  const missing = this.store.missingForSelection(entityId, fields);
1417
1451
  if (fetchAll || missing.size > 0) group.ids.push(raw);
1418
1452
  }
1453
+ } else if (isQueryItem(item)) {
1454
+ const { argsPayload, plan } = this.resolveSelection(item.view, item.args);
1455
+ const entityId = this.rootRequests.get(name);
1456
+ const missing = entityId ? this.store.missingForSelection(entityId, plan.paths) : plan.paths;
1457
+ if (fetchAll || missing.size > 0) promises.push(this.fetchQueryAndNormalize(name, item, plan, argsPayload));
1419
1458
  } else promises.push(this.fetchListAndNormalize(name, item));
1420
1459
  }
1421
1460
  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())]);
@@ -1424,13 +1463,19 @@ var FateClient = class {
1424
1463
  for (const [name, item] of Object.entries(request)) {
1425
1464
  const isNode = isNodeItem(item);
1426
1465
  if (isNode || isNodesItem(item)) {
1427
- const fields = getSelectionPlan(item.root, null).paths;
1466
+ const fields = getSelectionPlan(item.view, null).paths;
1428
1467
  for (const raw of isNode ? [item.id] : item.ids) {
1429
1468
  const entityId = toEntityId(item.type, raw);
1430
1469
  if (this.store.missingForSelection(entityId, fields).size > 0) return false;
1431
1470
  }
1432
1471
  continue;
1433
1472
  }
1473
+ if (isQueryItem(item)) {
1474
+ const { plan } = this.resolveSelection(item.view, item.args);
1475
+ const entityId = this.rootRequests.get(name);
1476
+ if ((entityId ? this.store.missingForSelection(entityId, plan.paths) : plan.paths).size > 0) return false;
1477
+ continue;
1478
+ }
1434
1479
  if (!this.store.getList(name)) return false;
1435
1480
  }
1436
1481
  return true;
@@ -1439,17 +1484,26 @@ var FateClient = class {
1439
1484
  const result = {};
1440
1485
  for (const [name, item] of Object.entries(request)) {
1441
1486
  if (isNodeItem(item)) {
1442
- result[name] = this.ref(item.type, item.id, item.root);
1487
+ result[name] = this.ref(item.type, item.id, item.view);
1443
1488
  continue;
1444
1489
  }
1445
1490
  if (isNodesItem(item)) {
1446
- result[name] = item.ids.map((id$1) => this.ref(item.type, id$1, item.root));
1491
+ result[name] = item.ids.map((id$1) => this.ref(item.type, id$1, item.view));
1492
+ continue;
1493
+ }
1494
+ if (isQueryItem(item)) {
1495
+ const entityId = this.rootRequests.get(name);
1496
+ if (entityId) {
1497
+ const { id: id$1 } = parseEntityId(entityId);
1498
+ result[name] = createRef(item.type, id$1, item.view, { root: true });
1499
+ } else result[name] = null;
1447
1500
  continue;
1448
1501
  }
1449
1502
  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;
1503
+ const hasItems = item.list && typeof item.list === "object" && "items" in item.list;
1504
+ const nodeView = hasItems && item.list.items ? item.list.items.node ?? item.list : item.list;
1451
1505
  const nodes = (listState?.ids ?? []).map((id$1) => this.rootListRef(id$1, nodeView));
1452
- if (item.root && typeof item.root === "object" && "items" in item.root) {
1506
+ if (hasItems) {
1453
1507
  const connection = {
1454
1508
  items: nodes.map((node, index) => ({
1455
1509
  cursor: listState?.cursors?.[index],
@@ -1457,7 +1511,7 @@ var FateClient = class {
1457
1511
  })),
1458
1512
  pagination: listState?.pagination
1459
1513
  };
1460
- const { argsPayload } = this.resolveListSelection(item.root, item.args);
1514
+ const { argsPayload } = this.resolveSelection(item.list, item.args);
1461
1515
  const metadata = {
1462
1516
  args: argsPayload,
1463
1517
  field: name,
@@ -1485,9 +1539,16 @@ var FateClient = class {
1485
1539
  const records = await this.transport.fetchById(type, ids, select, resolvedArgs);
1486
1540
  for (const record of records) this.writeEntity(type, record, select, void 0, plan, prefix);
1487
1541
  }
1542
+ async fetchQueryAndNormalize(name, item, plan, argsPayload) {
1543
+ if (!this.transport.fetchQuery) throw new Error(`fate: transport does not support queries. Please add support for 'fetchQuery' in your transport for '${name}'.`);
1544
+ const record = await this.transport.fetchQuery(name, plan.paths, argsPayload);
1545
+ if (!record || typeof record !== "object") return;
1546
+ const entityId = this.writeEntity(item.type, record, plan.paths, void 0, plan);
1547
+ this.rootRequests.set(name, entityId);
1548
+ }
1488
1549
  async fetchListAndNormalize(name, item) {
1489
1550
  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);
1551
+ const { argsPayload, plan } = this.resolveSelection(item.list, item.args);
1491
1552
  const { items, pagination } = await this.transport.fetchList(name, plan.paths, argsPayload);
1492
1553
  const ids = [];
1493
1554
  const cursors = [];
@@ -1496,13 +1557,14 @@ var FateClient = class {
1496
1557
  ids.push(id$1);
1497
1558
  cursors.push(entry.cursor);
1498
1559
  }
1560
+ this.registerRootList(item.type, name);
1499
1561
  this.store.setList(name, {
1500
1562
  cursors,
1501
1563
  ids,
1502
1564
  pagination
1503
1565
  });
1504
1566
  }
1505
- resolveListSelection(view$1, args) {
1567
+ resolveSelection(view$1, args) {
1506
1568
  const plan = getSelectionPlan(view$1, null);
1507
1569
  const argsPayload = combineArgsPayload(args, resolvedArgsFromPlan(plan));
1508
1570
  if (argsPayload) applyArgsPayloadToPlan(plan, argsPayload);
@@ -1511,7 +1573,7 @@ var FateClient = class {
1511
1573
  plan
1512
1574
  };
1513
1575
  }
1514
- writeEntity(type, record, select, snapshots, plan, pathPrefix = null, blockedMask) {
1576
+ writeEntity(type, record, select, snapshots, plan, pathPrefix = null, blockedMask, insert) {
1515
1577
  const config = this.types.get(type);
1516
1578
  if (!config) throw new Error(`fate: Found unknown entity type '${type}' in normalization.`);
1517
1579
  const entityId = toEntityId(type, config.getId(record));
@@ -1614,10 +1676,12 @@ var FateClient = class {
1614
1676
  if (snapshots && !snapshots.has(entityId)) snapshots.set(entityId, this.store.snapshot(entityId));
1615
1677
  this.viewDataCache.invalidate(entityId);
1616
1678
  this.store.merge(entityId, result, select);
1617
- this.linkParentLists(type, entityId, result, snapshots);
1679
+ this.linkParentLists(type, entityId, result, snapshots, insert ?? "after");
1680
+ if (!pathPrefix && insert) this.insertIntoRootLists(type, entityId, insert);
1618
1681
  return entityId;
1619
1682
  }
1620
- linkParentLists(type, entityId, record, snapshots) {
1683
+ linkParentLists(type, entityId, record, snapshots, insert) {
1684
+ if (insert === "none") return;
1621
1685
  const parents = this.parentLists.get(type);
1622
1686
  if (!parents) return;
1623
1687
  for (const parent of parents) {
@@ -1631,11 +1695,11 @@ var FateClient = class {
1631
1695
  if (current.some((item) => isNodeRef(item) && getNodeRefId(item) === entityId)) continue;
1632
1696
  if (snapshots && !snapshots.has(parentId)) snapshots.set(parentId, this.store.snapshot(parentId));
1633
1697
  this.viewDataCache.invalidate(parentId);
1634
- const nextList = [...current, createNodeRef(entityId)];
1698
+ const nextList = insert === "before" ? [createNodeRef(entityId), ...current] : [...current, createNodeRef(entityId)];
1635
1699
  const ids = nextList.map((item) => isNodeRef(item) ? getNodeRefId(item) : null).filter((id$1) => id$1 != null);
1636
1700
  const defaultListKey = getListKey(parentId, parent.field);
1637
1701
  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;
1702
+ const nextDefaultCursors = defaultListState?.cursors ? insert === "before" ? [void 0, ...defaultListState.cursors] : [...defaultListState.cursors, void 0] : defaultListState?.cursors;
1639
1703
  this.store.setList(defaultListKey, {
1640
1704
  cursors: nextDefaultCursors,
1641
1705
  ids,
@@ -1644,8 +1708,8 @@ var FateClient = class {
1644
1708
  const registeredLists = this.store.getListsForField(parentId, parent.field).filter(([key]) => key !== defaultListKey);
1645
1709
  for (const [listKey, listState] of registeredLists) {
1646
1710
  if (listState.ids.includes(entityId)) continue;
1647
- const listIds = [...listState.ids, entityId];
1648
- const listCursors = listState.cursors ? [...listState.cursors, void 0] : void 0;
1711
+ const listIds = insert === "before" ? [entityId, ...listState.ids] : [...listState.ids, entityId];
1712
+ const listCursors = listState.cursors ? insert === "before" ? [void 0, ...listState.cursors] : [...listState.cursors, void 0] : void 0;
1649
1713
  this.store.setList(listKey, {
1650
1714
  cursors: listCursors,
1651
1715
  ids: listIds,
@@ -1806,7 +1870,7 @@ function createClient(options) {
1806
1870
  * Builds a `Transport` backed by a tRPC client using the configured resolvers
1807
1871
  * for by-id queries, lists, and mutations.
1808
1872
  */
1809
- function createTRPCTransport({ byId, client, lists, mutations }) {
1873
+ function createTRPCTransport({ byId, client, lists, mutations, queries }) {
1810
1874
  const transport = {
1811
1875
  async fetchById(type, ids, select, args) {
1812
1876
  const resolver = byId[type];
@@ -1825,6 +1889,15 @@ function createTRPCTransport({ byId, client, lists, mutations }) {
1825
1889
  args,
1826
1890
  select: [...select]
1827
1891
  });
1892
+ },
1893
+ async fetchQuery(procedure, select, args) {
1894
+ if (!queries) throw new Error(`fate(trpc): No query resolvers configured; cannot call "${procedure}".`);
1895
+ const resolver = queries[procedure];
1896
+ if (!resolver) throw new Error(`fate(trpc): Missing query resolver for procedure "${procedure}"`);
1897
+ return resolver(client)({
1898
+ args,
1899
+ select: [...select]
1900
+ });
1828
1901
  }
1829
1902
  };
1830
1903
  transport.mutate = async (procedure, input, select) => {
package/lib/server.d.mts CHANGED
@@ -1,30 +1,28 @@
1
+ import { t as AnyRecord } from "./types-BPmcnouE.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 };