dynamodb-reactive 0.4.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.
package/dist/server.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { DynamoDBClient, ExecuteStatementCommand } from '@aws-sdk/client-dynamodb';
2
2
  import { GetCommand, PutCommand, DeleteCommand, DynamoDBDocumentClient, UpdateCommand, TransactWriteCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
3
3
  import { createHash, randomUUID } from 'crypto';
4
- import { z } from 'zod';
5
4
  import { unmarshall } from '@aws-sdk/util-dynamodb';
5
+ import { z } from 'zod';
6
6
  import { isDeepStrictEqual } from 'util';
7
7
  import { ApiGatewayManagementApiClient, PostToConnectionCommand, GoneException } from '@aws-sdk/client-apigatewaymanagementapi';
8
8
  import jsonpatch from 'fast-json-patch';
@@ -152,11 +152,9 @@ function encodeStructuredValue(value, seen) {
152
152
  return `function:${String(value)}`;
153
153
  }
154
154
  if (ArrayBuffer.isView(value)) {
155
- return `binary:${Buffer.from(
156
- value.buffer,
157
- value.byteOffset,
158
- value.byteLength
159
- ).toString("base64url")}`;
155
+ return `binary:${Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString(
156
+ "base64url"
157
+ )}`;
160
158
  }
161
159
  if (value instanceof ArrayBuffer) {
162
160
  return `binary:${Buffer.from(value).toString("base64url")}`;
@@ -168,9 +166,7 @@ function encodeStructuredValue(value, seen) {
168
166
  seen.add(value);
169
167
  let encoded;
170
168
  if (Array.isArray(value)) {
171
- encoded = `array:${JSON.stringify(
172
- value.map((item) => encodeStructuredValue(item, seen))
173
- )}`;
169
+ encoded = `array:${JSON.stringify(value.map((item) => encodeStructuredValue(item, seen)))}`;
174
170
  } else if (value instanceof Set) {
175
171
  encoded = `set:${JSON.stringify(
176
172
  [...value].map((item) => encodeStructuredValue(item, seen)).sort()
@@ -222,15 +218,13 @@ function extractFromCondition(tableName, condition) {
222
218
  return { dependencies, safe: dependencies.length === 1 };
223
219
  }
224
220
  case "logical": {
225
- if (condition.operator === "not" || !condition.conditions || condition.conditions.length === 0) {
221
+ if (condition.operator === "not" || // oxlint-disable-next-line typescript/no-unnecessary-condition -- Conditions can come from plain JavaScript callers.
222
+ !condition.conditions || condition.conditions.length === 0) {
226
223
  return { dependencies, safe: false };
227
224
  }
228
225
  let safe = true;
229
226
  for (const subCondition of condition.conditions) {
230
- const extracted = extractFromCondition(
231
- tableName,
232
- subCondition
233
- );
227
+ const extracted = extractFromCondition(tableName, subCondition);
234
228
  dependencies.push(...extracted.dependencies);
235
229
  safe &&= extracted.safe;
236
230
  }
@@ -255,11 +249,7 @@ function extractDependencies(operation) {
255
249
  return [...unique.values()];
256
250
  }
257
251
  function createDependencyKey(dependency) {
258
- const components = [
259
- dependency.tableName,
260
- dependency.fieldName,
261
- dependency.fieldValue
262
- ];
252
+ const components = [dependency.tableName, dependency.fieldName, dependency.fieldValue];
263
253
  const encoded = components.map(
264
254
  (component) => Buffer.from(component, "utf8").toString("base64url")
265
255
  );
@@ -311,9 +301,7 @@ function parseDependencyKey(key) {
311
301
  return null;
312
302
  }
313
303
  function extractAffectedKeys(tableName, item) {
314
- const keys = /* @__PURE__ */ new Set([
315
- createDependencyKey(wildcardDependency(tableName))
316
- ]);
304
+ const keys = /* @__PURE__ */ new Set([createDependencyKey(wildcardDependency(tableName))]);
317
305
  const visit = (fieldName, fieldValue) => {
318
306
  if (fieldValue !== null && fieldValue !== void 0) {
319
307
  const encodedValue = encodeDependencyValue(fieldValue);
@@ -327,10 +315,7 @@ function extractAffectedKeys(tableName, item) {
327
315
  );
328
316
  }
329
317
  if (typeof fieldValue === "string") {
330
- const maxPrefixLen = Math.min(
331
- fieldValue.length,
332
- MAX_TRACKED_PREFIX_LENGTH
333
- );
318
+ const maxPrefixLen = Math.min(fieldValue.length, MAX_TRACKED_PREFIX_LENGTH);
334
319
  for (let i = 1; i <= maxPrefixLen; i++) {
335
320
  keys.add(
336
321
  createDependencyKey({
@@ -687,6 +672,8 @@ var SystemTableNames = {
687
672
  dependencies: "ReactiveDependenciesV2",
688
673
  queries: "ReactiveConnectionQueriesV2"
689
674
  };
675
+
676
+ // ../server/src/query-metadata.ts
690
677
  var InvalidQueryMetadataError = class extends Error {
691
678
  constructor(message) {
692
679
  super(message);
@@ -739,9 +726,7 @@ function attributeValue(value) {
739
726
  )
740
727
  };
741
728
  }
742
- throw new InvalidQueryMetadataError(
743
- `Unsupported query value type: ${typeof value}`
744
- );
729
+ throw new InvalidQueryMetadataError(`Unsupported query value type: ${typeof value}`);
745
730
  }
746
731
  function quoteIdentifier(identifier) {
747
732
  if (!identifier) {
@@ -767,9 +752,7 @@ function compileCondition(condition, parameters) {
767
752
  };
768
753
  const operator = operators[condition.operator];
769
754
  if (!operator) {
770
- throw new InvalidQueryMetadataError(
771
- `Unsupported comparison operator: ${condition.operator}`
772
- );
755
+ throw new InvalidQueryMetadataError(`Unsupported comparison operator: ${condition.operator}`);
773
756
  }
774
757
  parameters.push(attributeValue(condition.value));
775
758
  return `${quotePath(condition.field)} ${operator} ?`;
@@ -786,7 +769,7 @@ function compileCondition(condition, parameters) {
786
769
  return `${name}(${quotePath(condition.field)}, ?)`;
787
770
  }
788
771
  throw new InvalidQueryMetadataError(
789
- `Unsupported function operator: ${condition.operator}`
772
+ `Unsupported function operator: ${String(condition.operator)}`
790
773
  );
791
774
  }
792
775
  if (condition.type === "logical" && condition.conditions?.length) {
@@ -806,9 +789,7 @@ function compileCondition(condition, parameters) {
806
789
  function parseQueryMetadata(metadata) {
807
790
  const parsed = QueryMetadataSchema.safeParse(metadata);
808
791
  if (!parsed.success) {
809
- throw new InvalidQueryMetadataError(
810
- `Invalid query metadata: ${parsed.error.message}`
811
- );
792
+ throw new InvalidQueryMetadataError(`Invalid query metadata: ${parsed.error.message}`);
812
793
  }
813
794
  return parsed.data;
814
795
  }
@@ -825,9 +806,7 @@ function compileQueryMetadata(metadataInput, nextToken) {
825
806
  }
826
807
  if (metadata.filterConditions.length) {
827
808
  predicates.push(
828
- ...metadata.filterConditions.map(
829
- (condition) => compileCondition(condition, parameters)
830
- )
809
+ ...metadata.filterConditions.map((condition) => compileCondition(condition, parameters))
831
810
  );
832
811
  }
833
812
  if (predicates.length) statement += ` WHERE ${predicates.join(" AND ")}`;
@@ -867,9 +846,7 @@ async function executeQueryMetadata(client, metadataInput) {
867
846
  new ExecuteStatementCommand(compileQueryMetadata(metadata, nextToken))
868
847
  );
869
848
  items.push(
870
- ...(response.Items ?? []).map(
871
- (item) => unmarshall(item)
872
- )
849
+ ...(response.Items ?? []).map((item) => unmarshall(item))
873
850
  );
874
851
  nextToken = response.NextToken;
875
852
  if (nextToken && seenTokens.has(nextToken)) {
@@ -915,10 +892,7 @@ function createClients(config) {
915
892
  function createDbContext(config, dependencyTracker) {
916
893
  const { client, docClient } = createClients(config);
917
894
  async function executeQuery(operation) {
918
- return executeQueryMetadata(
919
- client,
920
- operationToQueryMetadata(operation)
921
- );
895
+ return executeQueryMetadata(client, operationToQueryMetadata(operation));
922
896
  }
923
897
  return {
924
898
  query(table) {
@@ -969,10 +943,7 @@ function createDbContext(config, dependencyTracker) {
969
943
  });
970
944
  const expected = current;
971
945
  const fields = [
972
- .../* @__PURE__ */ new Set([
973
- ...table.getFieldNames().map(String),
974
- ...Object.keys(expected)
975
- ])
946
+ .../* @__PURE__ */ new Set([...table.getFieldNames().map(String), ...Object.keys(expected)])
976
947
  ].sort();
977
948
  const conditions = [];
978
949
  const expressionAttributeNames = {};
@@ -997,9 +968,7 @@ function createDbContext(config, dependencyTracker) {
997
968
  "utf8"
998
969
  );
999
970
  if (Buffer.byteLength(conditionExpression, "utf8") > MAX_CONDITION_EXPRESSION_BYTES || conditions.length > MAX_CONDITION_OPERATORS || variableBytes > MAX_EXPRESSION_VARIABLE_BYTES) {
1000
- throw new Error(
1001
- "Item is too complex to update with a safe compare-and-swap"
1002
- );
971
+ throw new Error("Item is too complex to update with a safe compare-and-swap");
1003
972
  }
1004
973
  const command = new PutCommand({
1005
974
  TableName: table.tableName,
@@ -1064,26 +1033,11 @@ function validateKeyString(name, value, maxBytes) {
1064
1033
  }
1065
1034
  }
1066
1035
  function validateRequest(request) {
1067
- validateKeyString(
1068
- "connectionId",
1069
- request.connectionId,
1070
- DYNAMODB_PARTITION_KEY_MAX_BYTES2
1071
- );
1036
+ validateKeyString("connectionId", request.connectionId, DYNAMODB_PARTITION_KEY_MAX_BYTES2);
1072
1037
  if (request.type === "subscribe" || request.type === "unsubscribe") {
1073
- validateKeyString(
1074
- "subscriptionId",
1075
- request.subscriptionId,
1076
- DYNAMODB_SORT_KEY_MAX_BYTES
1077
- );
1078
- validateKeyString(
1079
- "registrationId",
1080
- request.registrationId,
1081
- DYNAMODB_SORT_KEY_MAX_BYTES
1082
- );
1083
- if (Buffer.byteLength(
1084
- `${request.connectionId}#${request.subscriptionId}`,
1085
- "utf8"
1086
- ) > DYNAMODB_SORT_KEY_MAX_BYTES) {
1038
+ validateKeyString("subscriptionId", request.subscriptionId, DYNAMODB_SORT_KEY_MAX_BYTES);
1039
+ validateKeyString("registrationId", request.registrationId, DYNAMODB_SORT_KEY_MAX_BYTES);
1040
+ if (Buffer.byteLength(`${request.connectionId}#${request.subscriptionId}`, "utf8") > DYNAMODB_SORT_KEY_MAX_BYTES) {
1087
1041
  throw requestError(
1088
1042
  "connectionId and subscriptionId exceed DynamoDB dependency key limits",
1089
1043
  400
@@ -1216,18 +1170,11 @@ function createReactiveHandler(config) {
1216
1170
  if (procedure.type !== "query") {
1217
1171
  throw requestError("Only query procedures can be subscribed to", 400);
1218
1172
  }
1219
- const resolverResult = await config.router.execute(
1220
- request.path,
1221
- context,
1222
- request.input
1223
- );
1173
+ const resolverResult = await config.router.execute(request.path, context, request.input);
1224
1174
  const operations = tracker.getOperations();
1225
1175
  const capturedResult = tracker.getCapturedResult();
1226
1176
  if (operations.length !== 1 || !capturedResult) {
1227
- throw requestError(
1228
- "A subscription must execute exactly one ctx.db query",
1229
- 400
1230
- );
1177
+ throw requestError("A subscription must execute exactly one ctx.db query", 400);
1231
1178
  }
1232
1179
  if (!Array.isArray(resolverResult) || resolverResult !== capturedResult.reference || !isDeepStrictEqual(resolverResult, capturedResult.snapshot)) {
1233
1180
  throw requestError(
@@ -1394,10 +1341,7 @@ function createReactiveHandler(config) {
1394
1341
  }
1395
1342
  let baseline;
1396
1343
  try {
1397
- baseline = await executeQueryMetadata(
1398
- ddbClient,
1399
- registrationEntry.queryMetadata
1400
- );
1344
+ baseline = await executeQueryMetadata(ddbClient, registrationEntry.queryMetadata);
1401
1345
  const active = {
1402
1346
  ...registrationEntry,
1403
1347
  status: "active",
@@ -1461,9 +1405,7 @@ function createReactiveHandler(config) {
1461
1405
  )
1462
1406
  ]);
1463
1407
  const query2 = queryResponse.Item;
1464
- const currentConnection = SystemSchemas.connections.safeParse(
1465
- connectionResponse.Item
1466
- );
1408
+ const currentConnection = SystemSchemas.connections.safeParse(connectionResponse.Item);
1467
1409
  if (query2?.status !== "active" || query2.generation !== request.generation || query2.registrationId !== request.registrationId || !currentConnection.success || currentConnection.data.status !== "open" || currentConnection.data.connectedAt !== connection.connectedAt) {
1468
1410
  return null;
1469
1411
  }
@@ -1563,11 +1505,7 @@ function createReactiveHandler(config) {
1563
1505
  };
1564
1506
  }
1565
1507
  async function registerConnection(connectionId, context) {
1566
- validateKeyString(
1567
- "connectionId",
1568
- connectionId,
1569
- DYNAMODB_PARTITION_KEY_MAX_BYTES2
1570
- );
1508
+ validateKeyString("connectionId", connectionId, DYNAMODB_PARTITION_KEY_MAX_BYTES2);
1571
1509
  const now = Date.now();
1572
1510
  await docClient.send(
1573
1511
  new PutCommand({
@@ -1583,11 +1521,7 @@ function createReactiveHandler(config) {
1583
1521
  );
1584
1522
  }
1585
1523
  async function unregisterConnection(connectionId) {
1586
- validateKeyString(
1587
- "connectionId",
1588
- connectionId,
1589
- DYNAMODB_PARTITION_KEY_MAX_BYTES2
1590
- );
1524
+ validateKeyString("connectionId", connectionId, DYNAMODB_PARTITION_KEY_MAX_BYTES2);
1591
1525
  await docClient.send(
1592
1526
  new UpdateCommand({
1593
1527
  TableName: connectionsTable,
@@ -1658,9 +1592,8 @@ function createReactiveHandler(config) {
1658
1592
  createCaller
1659
1593
  };
1660
1594
  }
1661
- var { compare } = jsonpatch;
1662
1595
  function generatePatches(oldValue, newValue) {
1663
- const operations = compare(
1596
+ const operations = jsonpatch.compare(
1664
1597
  oldValue,
1665
1598
  newValue
1666
1599
  );
@@ -1691,13 +1624,7 @@ function isConditionalFailure(error2) {
1691
1624
  return error2 instanceof Error && error2.name === "ConditionalCheckFailedException";
1692
1625
  }
1693
1626
  function createStreamUtils(config) {
1694
- const {
1695
- ddbClient,
1696
- docClient,
1697
- connectionsTable,
1698
- dependenciesTable,
1699
- queriesTable
1700
- } = config;
1627
+ const { ddbClient, docClient, connectionsTable, dependenciesTable, queriesTable } = config;
1701
1628
  let apiClient = config.apiClient;
1702
1629
  function getApiClient() {
1703
1630
  apiClient ??= new ApiGatewayManagementApiClient({
@@ -1873,10 +1800,7 @@ function createStreamUtils(config) {
1873
1800
  messageId: randomUUID(),
1874
1801
  patches
1875
1802
  };
1876
- const pendingDelivery = Buffer.byteLength(
1877
- JSON.stringify(deliveryMessage(query, patchDelivery)),
1878
- "utf8"
1879
- ) > MAX_PATCH_MESSAGE_BYTES ? {
1803
+ const pendingDelivery = Buffer.byteLength(JSON.stringify(deliveryMessage(query, patchDelivery)), "utf8") > MAX_PATCH_MESSAGE_BYTES ? {
1880
1804
  type: "resync",
1881
1805
  generation: query.generation,
1882
1806
  fromVersion,
@@ -2054,17 +1978,14 @@ function assertConnectionId(connectionId) {
2054
1978
  }
2055
1979
  }
2056
1980
  async function mapWithConcurrency(values, concurrency, mapper) {
2057
- const results = new Array(values.length);
1981
+ const results = Array.from({ length: values.length });
2058
1982
  let nextIndex = 0;
2059
- const workers = Array.from(
2060
- { length: Math.min(concurrency, values.length) },
2061
- async () => {
2062
- while (nextIndex < values.length) {
2063
- const index = nextIndex++;
2064
- results[index] = await mapper(values[index], index);
2065
- }
1983
+ const workers = Array.from({ length: Math.min(concurrency, values.length) }, async () => {
1984
+ while (nextIndex < values.length) {
1985
+ const index = nextIndex++;
1986
+ results[index] = await mapper(values[index], index);
2066
1987
  }
2067
- );
1988
+ });
2068
1989
  await Promise.all(workers);
2069
1990
  return results;
2070
1991
  }
@@ -2082,10 +2003,7 @@ function extractRecordAffectedKeys(record) {
2082
2003
  return affectedKeys;
2083
2004
  }
2084
2005
  function subscriptionKey(subscription) {
2085
- return JSON.stringify([
2086
- subscription.connectionId,
2087
- subscription.subscriptionId
2088
- ]);
2006
+ return JSON.stringify([subscription.connectionId, subscription.subscriptionId]);
2089
2007
  }
2090
2008
  function failureIdentifier(record, cause) {
2091
2009
  const itemIdentifier = record.dynamodb?.SequenceNumber;
@@ -2187,9 +2105,7 @@ function createConnectHandler(config) {
2187
2105
  connectedAt: now,
2188
2106
  ttl: Math.floor(now / 1e3) + DEFAULT_CONNECTION_LEASE_SECONDS
2189
2107
  };
2190
- await docClient.send(
2191
- new PutCommand({ TableName: connectionsTable, Item: connectionEntry })
2192
- );
2108
+ await docClient.send(new PutCommand({ TableName: connectionsTable, Item: connectionEntry }));
2193
2109
  debug("[stream] Connection established");
2194
2110
  return { statusCode: 200 };
2195
2111
  } catch (error2) {
@@ -2254,18 +2170,9 @@ function assertRequestKey(name, value, maxBytes) {
2254
2170
  return value;
2255
2171
  }
2256
2172
  function createLambdaHandlers(config = {}) {
2257
- const connectionsTable = requiredTableName(
2258
- config.connectionsTableName,
2259
- "CONNECTIONS_TABLE"
2260
- );
2261
- const dependenciesTable = requiredTableName(
2262
- config.dependenciesTableName,
2263
- "DEPENDENCIES_TABLE"
2264
- );
2265
- const queriesTable = requiredTableName(
2266
- config.queriesTableName,
2267
- "QUERIES_TABLE"
2268
- );
2173
+ const connectionsTable = requiredTableName(config.connectionsTableName, "CONNECTIONS_TABLE");
2174
+ const dependenciesTable = requiredTableName(config.dependenciesTableName, "DEPENDENCIES_TABLE");
2175
+ const queriesTable = requiredTableName(config.queriesTableName, "QUERIES_TABLE");
2269
2176
  const apiGatewayEndpoint = config.apiGatewayEndpoint ?? process.env.WEBSOCKET_ENDPOINT ?? "";
2270
2177
  const ddbClient = config.dbConfig?.client ?? new DynamoDBClient({
2271
2178
  region: config.dbConfig?.region ?? process.env.AWS_REGION,