monorise 1.0.0 → 1.1.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/base/index.d.ts +110 -3
  2. package/dist/base/index.js +3 -1
  3. package/dist/base/index.js.map +1 -1
  4. package/dist/cli/cli.js +6 -0
  5. package/dist/cli/cli.js.map +1 -1
  6. package/dist/core/chunk-QV4Q5377.js +76 -0
  7. package/dist/core/chunk-QV4Q5377.js.map +1 -0
  8. package/dist/core/index.d.ts +191 -38
  9. package/dist/core/index.js +1539 -122
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/core/service.config-ZJEZ6EKA.js +13 -0
  12. package/dist/core/service.config-ZJEZ6EKA.js.map +1 -0
  13. package/dist/proxy/index.d.ts +35 -0
  14. package/dist/proxy/index.js +75 -0
  15. package/dist/proxy/index.js.map +1 -0
  16. package/dist/react/actions/websocket.action.d.ts +71 -0
  17. package/dist/react/actions/websocket.action.d.ts.map +1 -0
  18. package/dist/react/chunk-2QOYO3GF.js +182 -0
  19. package/dist/react/chunk-2QOYO3GF.js.map +1 -0
  20. package/dist/react/chunk-4WSYM746.js +383 -0
  21. package/dist/react/chunk-4WSYM746.js.map +1 -0
  22. package/dist/react/chunk-5XIRNUBL.js +43 -0
  23. package/dist/react/chunk-5XIRNUBL.js.map +1 -0
  24. package/dist/react/chunk-7JDOKZGQ.js +172 -0
  25. package/dist/react/chunk-7JDOKZGQ.js.map +1 -0
  26. package/dist/react/chunk-A5TI2FW3.js +13 -0
  27. package/dist/react/chunk-A5TI2FW3.js.map +1 -0
  28. package/dist/react/chunk-BJXCFDMF.js +15 -0
  29. package/dist/react/chunk-BJXCFDMF.js.map +1 -0
  30. package/dist/react/chunk-BUTF5RJU.js +29 -0
  31. package/dist/react/chunk-BUTF5RJU.js.map +1 -0
  32. package/dist/react/chunk-DTRWUIDH.js +402 -0
  33. package/dist/react/chunk-DTRWUIDH.js.map +1 -0
  34. package/dist/react/chunk-GFVCNWVT.js +54 -0
  35. package/dist/react/chunk-GFVCNWVT.js.map +1 -0
  36. package/dist/react/chunk-JT5EZZSL.js +489 -0
  37. package/dist/react/chunk-JT5EZZSL.js.map +1 -0
  38. package/dist/react/chunk-KJX5LOMN.js +43 -0
  39. package/dist/react/chunk-KJX5LOMN.js.map +1 -0
  40. package/dist/react/chunk-KLXK4V6G.js +65 -0
  41. package/dist/react/chunk-KLXK4V6G.js.map +1 -0
  42. package/dist/react/chunk-LJLMKEKI.js +245 -0
  43. package/dist/react/chunk-LJLMKEKI.js.map +1 -0
  44. package/dist/react/chunk-MIXAYX55.js +147 -0
  45. package/dist/react/chunk-MIXAYX55.js.map +1 -0
  46. package/dist/react/chunk-RPNCWADG.js +248 -0
  47. package/dist/react/chunk-RPNCWADG.js.map +1 -0
  48. package/dist/react/chunk-S6RDMHHH.js +47 -0
  49. package/dist/react/chunk-S6RDMHHH.js.map +1 -0
  50. package/dist/react/chunk-U6RIOMF4.js +893 -0
  51. package/dist/react/chunk-U6RIOMF4.js.map +1 -0
  52. package/dist/react/chunk-WCRLJFBW.js +5568 -0
  53. package/dist/react/chunk-WCRLJFBW.js.map +1 -0
  54. package/dist/react/chunk-YF6S7S36.js +588 -0
  55. package/dist/react/chunk-YF6S7S36.js.map +1 -0
  56. package/dist/react/dist-es-5WYA7CWK.js +379 -0
  57. package/dist/react/dist-es-5WYA7CWK.js.map +1 -0
  58. package/dist/react/dist-es-CR5AOOCO.js +333 -0
  59. package/dist/react/dist-es-CR5AOOCO.js.map +1 -0
  60. package/dist/react/dist-es-KZ3GLAJI.js +494 -0
  61. package/dist/react/dist-es-KZ3GLAJI.js.map +1 -0
  62. package/dist/react/dist-es-R4TRTT45.js +22 -0
  63. package/dist/react/dist-es-R4TRTT45.js.map +1 -0
  64. package/dist/react/dist-es-SKDPAJEW.js +169 -0
  65. package/dist/react/dist-es-SKDPAJEW.js.map +1 -0
  66. package/dist/react/dist-es-TOHBZNTZ.js +71 -0
  67. package/dist/react/dist-es-TOHBZNTZ.js.map +1 -0
  68. package/dist/react/dist-es-XNAC47MK.js +90 -0
  69. package/dist/react/dist-es-XNAC47MK.js.map +1 -0
  70. package/dist/react/event-streams-WAZW4P3K.js +277 -0
  71. package/dist/react/event-streams-WAZW4P3K.js.map +1 -0
  72. package/dist/react/index.d.ts +53 -4
  73. package/dist/react/index.d.ts.map +1 -1
  74. package/dist/react/index.js +10991 -190
  75. package/dist/react/index.js.map +1 -1
  76. package/dist/react/loadSso-KXVD6CBM.js +556 -0
  77. package/dist/react/loadSso-KXVD6CBM.js.map +1 -0
  78. package/dist/react/service.config-I7RKP6FE.js +14 -0
  79. package/dist/react/service.config-I7RKP6FE.js.map +1 -0
  80. package/dist/react/services/core.service.d.ts +11 -1
  81. package/dist/react/services/core.service.d.ts.map +1 -1
  82. package/dist/react/signin-SEY3FDQ5.js +653 -0
  83. package/dist/react/signin-SEY3FDQ5.js.map +1 -0
  84. package/dist/react/sso-oidc-REODVHH5.js +786 -0
  85. package/dist/react/sso-oidc-REODVHH5.js.map +1 -0
  86. package/dist/react/sts-I3M4QP37.js +3948 -0
  87. package/dist/react/sts-I3M4QP37.js.map +1 -0
  88. package/dist/react/websocket/WebSocketManager.d.ts +68 -0
  89. package/dist/react/websocket/WebSocketManager.d.ts.map +1 -0
  90. package/dist/react/websocket/index.d.ts +3 -0
  91. package/dist/react/websocket/index.d.ts.map +1 -0
  92. package/dist/react/websocket/optimistic.d.ts +51 -0
  93. package/dist/react/websocket/optimistic.d.ts.map +1 -0
  94. package/dist/react/websocket-OSLLJSNO.js +10 -0
  95. package/dist/react/websocket-OSLLJSNO.js.map +1 -0
  96. package/dist/sst/components/monorise-core.d.ts +10 -0
  97. package/dist/sst/components/monorise-core.d.ts.map +1 -1
  98. package/dist/sst/index.js +75 -12
  99. package/dist/sst/index.js.map +1 -1
  100. package/package.json +9 -1
@@ -1,58 +1,14 @@
1
- var __defProp = Object.defineProperty;
2
- var __defProps = Object.defineProperties;
3
- var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
4
- var __getOwnPropSymbols = Object.getOwnPropertySymbols;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __propIsEnum = Object.prototype.propertyIsEnumerable;
7
- var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
8
- var __spreadValues = (a, b) => {
9
- for (var prop in b || (b = {}))
10
- if (__hasOwnProp.call(b, prop))
11
- __defNormalProp(a, prop, b[prop]);
12
- if (__getOwnPropSymbols)
13
- for (var prop of __getOwnPropSymbols(b)) {
14
- if (__propIsEnum.call(b, prop))
15
- __defNormalProp(a, prop, b[prop]);
16
- }
17
- return a;
18
- };
19
- var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
20
- var __objRest = (source, exclude) => {
21
- var target = {};
22
- for (var prop in source)
23
- if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
24
- target[prop] = source[prop];
25
- if (source != null && __getOwnPropSymbols)
26
- for (var prop of __getOwnPropSymbols(source)) {
27
- if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
28
- target[prop] = source[prop];
29
- }
30
- return target;
31
- };
32
- var __export = (target, all) => {
33
- for (var name in all)
34
- __defProp(target, name, { get: all[name], enumerable: true });
35
- };
36
- var __async = (__this, __arguments, generator) => {
37
- return new Promise((resolve, reject) => {
38
- var fulfilled = (value) => {
39
- try {
40
- step(generator.next(value));
41
- } catch (e) {
42
- reject(e);
43
- }
44
- };
45
- var rejected = (value) => {
46
- try {
47
- step(generator.throw(value));
48
- } catch (e) {
49
- reject(e);
50
- }
51
- };
52
- var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
53
- step((generator = generator.apply(__this, __arguments)).next());
54
- });
55
- };
1
+ import {
2
+ CORE_EVENT_BUS,
3
+ CORE_TABLE,
4
+ ENTITY_REPLICATION_INDEX,
5
+ MUTUAL_REPLICATION_INDEX,
6
+ __async,
7
+ __export,
8
+ __objRest,
9
+ __spreadProps,
10
+ __spreadValues
11
+ } from "./chunk-QV4Q5377.js";
56
12
 
57
13
  // controllers/setupRoutes.ts
58
14
  import { Hono } from "hono";
@@ -140,6 +96,11 @@ var setupCommonRoutes = (container) => {
140
96
  container.deleteEntityController.controller
141
97
  );
142
98
  app.get("/tag/:entityType/:tagName", container.listTagsController.controller);
99
+ app.post("/transaction", container.executeTransactionController.controller);
100
+ app.post(
101
+ "/ws/ticket/:entityType/:entityId",
102
+ container.createTicketController.controller
103
+ );
143
104
  return app;
144
105
  };
145
106
 
@@ -175,6 +136,7 @@ var StandardErrorCode = {
175
136
  ENTITY_ID_IS_UNDEFINED: "ENTITY_ID_IS_UNDEFINED",
176
137
  ENTITY_IS_UNDEFINED: "ENTITY_IS_UNDEFINED",
177
138
  ENTITY_NOT_FOUND: "ENTITY_NOT_FOUND",
139
+ INVALID_CONDITION: "INVALID_CONDITION",
178
140
  INVALID_ENTITY_TYPE: "INVALID_ENTITY_TYPE",
179
141
  INVALID_MUTUAL: "INVALID_MUTUAL",
180
142
  INVALID_QUERY: "INVALID_QUERY",
@@ -187,7 +149,10 @@ var StandardErrorCode = {
187
149
  REPLICATION_ERROR: "REPLICATION_ERROR",
188
150
  RETRYABLE_MUTUAL_LOCK_CONFLICT: "RETRYABLE_MUTUAL_LOCK_CONFLICT",
189
151
  TAG_IS_UNDEFINED: "TAG_IS_UNDEFINED",
152
+ TRANSACTION_EMPTY: "TRANSACTION_EMPTY",
190
153
  TRANSACTION_FAILED: "TRANSACTION_FAILED",
154
+ TRANSACTION_ITEM_LIMIT_EXCEEDED: "TRANSACTION_ITEM_LIMIT_EXCEEDED",
155
+ TRANSACTION_UNIQUE_FIELD_UPDATE: "TRANSACTION_UNIQUE_FIELD_UPDATE",
191
156
  UNIQUE_VALUE_EXISTS: "UNIQUE_VALUE_EXISTS"
192
157
  };
193
158
 
@@ -832,22 +797,22 @@ var EntityRepository = class extends Repository {
832
797
  }
833
798
  });
834
799
  }
835
- adjustEntity(entityType, entityId, adjustments, constraints) {
800
+ adjustEntity(entityType, entityId, adjustments, opts) {
836
801
  return __async(this, null, function* () {
837
802
  const entity = new Entity(entityType, entityId);
838
- const { UpdateExpression, ConditionExpression, ExpressionAttributeNames, ExpressionAttributeValues } = this.toAdjustUpdate(adjustments, constraints);
803
+ const { UpdateExpression, ExpressionAttributeNames, ExpressionAttributeValues } = this.toAdjustUpdate(adjustments);
839
804
  const updatedAtExpression = ", #updatedAt = :updatedAt";
840
805
  ExpressionAttributeNames["#updatedAt"] = "updatedAt";
841
806
  ExpressionAttributeValues[":updatedAt"] = { S: (/* @__PURE__ */ new Date()).toISOString() };
842
- const resp = yield this.dynamodbClient.updateItem(__spreadProps(__spreadValues({
807
+ const resp = yield this.dynamodbClient.updateItem({
843
808
  TableName: this.TABLE_NAME,
844
809
  Key: entity.keys(),
845
- UpdateExpression: UpdateExpression + updatedAtExpression
846
- }, ConditionExpression && { ConditionExpression }), {
847
- ExpressionAttributeNames,
848
- ExpressionAttributeValues,
810
+ UpdateExpression: UpdateExpression + updatedAtExpression,
811
+ ConditionExpression: (opts == null ? void 0 : opts.ConditionExpression) || "attribute_exists(PK)",
812
+ ExpressionAttributeNames: __spreadValues(__spreadValues({}, ExpressionAttributeNames), opts == null ? void 0 : opts.ExpressionAttributeNames),
813
+ ExpressionAttributeValues: __spreadValues(__spreadValues({}, ExpressionAttributeValues), opts == null ? void 0 : opts.ExpressionAttributeValues),
849
814
  ReturnValues: "ALL_NEW"
850
- }));
815
+ });
851
816
  return Entity.fromItem(resp.Attributes);
852
817
  });
853
818
  }
@@ -1857,6 +1822,217 @@ var TagRepository = class extends Repository {
1857
1822
  }
1858
1823
  };
1859
1824
 
1825
+ // data/WebSocket.ts
1826
+ import {
1827
+ DeleteCommand,
1828
+ DynamoDBDocumentClient,
1829
+ PutCommand,
1830
+ QueryCommand
1831
+ } from "@aws-sdk/lib-dynamodb";
1832
+ var WebSocketRepository = class extends Repository {
1833
+ constructor(tableName, dynamodbClient) {
1834
+ super();
1835
+ this.tableName = tableName;
1836
+ this.dynamodbClient = dynamodbClient;
1837
+ this.docClient = DynamoDBDocumentClient.from(dynamodbClient);
1838
+ }
1839
+ createConnection(connectionId, metadata, expiresAt) {
1840
+ return __async(this, null, function* () {
1841
+ yield this.docClient.send(
1842
+ new PutCommand({
1843
+ TableName: this.tableName,
1844
+ Item: __spreadProps(__spreadValues({
1845
+ PK: `CONN#${connectionId}`,
1846
+ SK: "#METADATA#",
1847
+ connectionId
1848
+ }, metadata), {
1849
+ expiresAt
1850
+ })
1851
+ })
1852
+ );
1853
+ });
1854
+ }
1855
+ getConnection(connectionId) {
1856
+ return __async(this, null, function* () {
1857
+ var _a;
1858
+ const result = yield this.docClient.send(
1859
+ new QueryCommand({
1860
+ TableName: this.tableName,
1861
+ KeyConditionExpression: "PK = :pk",
1862
+ ExpressionAttributeValues: {
1863
+ ":pk": `CONN#${connectionId}`
1864
+ }
1865
+ })
1866
+ );
1867
+ return (_a = result.Items) == null ? void 0 : _a[0];
1868
+ });
1869
+ }
1870
+ deleteConnection(connectionId) {
1871
+ return __async(this, null, function* () {
1872
+ yield this.docClient.send(
1873
+ new DeleteCommand({
1874
+ TableName: this.tableName,
1875
+ Key: {
1876
+ PK: `CONN#${connectionId}`,
1877
+ SK: "#METADATA#"
1878
+ }
1879
+ })
1880
+ );
1881
+ });
1882
+ }
1883
+ createSubscription(subKey, connectionId, data) {
1884
+ return __async(this, null, function* () {
1885
+ yield this.docClient.send(
1886
+ new PutCommand({
1887
+ TableName: this.tableName,
1888
+ Item: __spreadValues({
1889
+ PK: subKey,
1890
+ SK: `CONN#${connectionId}`,
1891
+ R1PK: `CONN#${connectionId}`,
1892
+ R1SK: subKey,
1893
+ connectionId
1894
+ }, data)
1895
+ })
1896
+ );
1897
+ });
1898
+ }
1899
+ deleteSubscription(subKey, connectionId) {
1900
+ return __async(this, null, function* () {
1901
+ yield this.docClient.send(
1902
+ new DeleteCommand({
1903
+ TableName: this.tableName,
1904
+ Key: {
1905
+ PK: subKey,
1906
+ SK: `CONN#${connectionId}`
1907
+ }
1908
+ })
1909
+ );
1910
+ });
1911
+ }
1912
+ querySubscriptionsByKey(subKey) {
1913
+ return __async(this, null, function* () {
1914
+ const result = yield this.docClient.send(
1915
+ new QueryCommand({
1916
+ TableName: this.tableName,
1917
+ KeyConditionExpression: "PK = :pk",
1918
+ ExpressionAttributeValues: {
1919
+ ":pk": subKey
1920
+ },
1921
+ ConsistentRead: true
1922
+ })
1923
+ );
1924
+ return result.Items || [];
1925
+ });
1926
+ }
1927
+ querySubscriptionsByConnectionId(connectionId) {
1928
+ return __async(this, null, function* () {
1929
+ const { ENTITY_REPLICATION_INDEX: ENTITY_REPLICATION_INDEX2 } = yield import("./service.config-ZJEZ6EKA.js");
1930
+ const result = yield this.docClient.send(
1931
+ new QueryCommand({
1932
+ TableName: this.tableName,
1933
+ IndexName: ENTITY_REPLICATION_INDEX2,
1934
+ KeyConditionExpression: "R1PK = :r1pk",
1935
+ ExpressionAttributeValues: {
1936
+ ":r1pk": `CONN#${connectionId}`
1937
+ }
1938
+ })
1939
+ );
1940
+ return result.Items || [];
1941
+ });
1942
+ }
1943
+ createTicket(ticket, entityType, entityId, feedTypes, expiresAt) {
1944
+ return __async(this, null, function* () {
1945
+ yield this.docClient.send(
1946
+ new PutCommand({
1947
+ TableName: this.tableName,
1948
+ Item: {
1949
+ PK: `TICKET#${ticket}`,
1950
+ SK: "#METADATA#",
1951
+ entityType,
1952
+ entityId,
1953
+ feedTypes,
1954
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1955
+ expiresAt
1956
+ }
1957
+ })
1958
+ );
1959
+ });
1960
+ }
1961
+ consumeTicket(ticket) {
1962
+ return __async(this, null, function* () {
1963
+ try {
1964
+ const result = yield this.docClient.send(
1965
+ new DeleteCommand({
1966
+ TableName: this.tableName,
1967
+ Key: {
1968
+ PK: `TICKET#${ticket}`,
1969
+ SK: "#METADATA#"
1970
+ },
1971
+ ConditionExpression: "attribute_exists(PK)",
1972
+ ReturnValues: "ALL_OLD"
1973
+ })
1974
+ );
1975
+ const item = result.Attributes;
1976
+ if (!item) return null;
1977
+ const expiresAt = item.expiresAt;
1978
+ if (expiresAt && expiresAt < Math.floor(Date.now() / 1e3)) {
1979
+ return null;
1980
+ }
1981
+ return {
1982
+ entityType: item.entityType,
1983
+ entityId: item.entityId,
1984
+ feedTypes: item.feedTypes || []
1985
+ };
1986
+ } catch (error) {
1987
+ if (error instanceof Error && error.name === "ConditionalCheckFailedException") {
1988
+ return null;
1989
+ }
1990
+ throw error;
1991
+ }
1992
+ });
1993
+ }
1994
+ queryMutualConnections(byEntityType, byEntityId) {
1995
+ return __async(this, null, function* () {
1996
+ const result = yield this.docClient.send(
1997
+ new QueryCommand({
1998
+ TableName: this.tableName,
1999
+ KeyConditionExpression: "PK = :pk",
2000
+ ExpressionAttributeValues: {
2001
+ ":pk": `${byEntityType}#${byEntityId}`
2002
+ },
2003
+ ProjectionExpression: "SK",
2004
+ ConsistentRead: true
2005
+ })
2006
+ );
2007
+ const connections = [];
2008
+ for (const item of result.Items || []) {
2009
+ const sk = item.SK;
2010
+ if (!sk || sk === "#METADATA#" || sk.startsWith("#")) continue;
2011
+ const parts = sk.split("#");
2012
+ if (parts.length >= 2) {
2013
+ connections.push({ entityType: parts[0], entityId: parts[1] });
2014
+ }
2015
+ }
2016
+ return connections;
2017
+ });
2018
+ }
2019
+ queryFeedSubscriptions(entityType, entityId) {
2020
+ return __async(this, null, function* () {
2021
+ const result = yield this.docClient.send(
2022
+ new QueryCommand({
2023
+ TableName: this.tableName,
2024
+ KeyConditionExpression: "PK = :pk",
2025
+ ExpressionAttributeValues: {
2026
+ ":pk": `SUB#FEED#${entityType}#${entityId}`
2027
+ },
2028
+ ConsistentRead: true
2029
+ })
2030
+ );
2031
+ return result.Items || [];
2032
+ });
2033
+ }
2034
+ };
2035
+
1860
2036
  // handles/app.ts
1861
2037
  import { Hono as Hono2 } from "hono";
1862
2038
  import { handle } from "hono/aws-lambda";
@@ -1896,14 +2072,6 @@ import {
1896
2072
  PutEventsCommand
1897
2073
  } from "@aws-sdk/client-eventbridge";
1898
2074
 
1899
- // constants/table.ts
1900
- var ENTITY_REPLICATION_INDEX = "ENTITY_REPLICATION_INDEX";
1901
- var MUTUAL_REPLICATION_INDEX = "MUTUAL_REPLICATION_INDEX";
1902
-
1903
- // configs/service.config.ts
1904
- var CORE_TABLE = process.env.CORE_TABLE || "";
1905
- var CORE_EVENT_BUS = process.env.CORE_EVENT_BUS || "";
1906
-
1907
2075
  // types/event.ts
1908
2076
  var SOURCE = {
1909
2077
  CORE: "core-service",
@@ -2147,7 +2315,7 @@ var handler2 = (container) => (ev) => __async(null, null, function* () {
2147
2315
  const { entityRepository, mutualRepository, publishEvent: publishEvent2 } = container;
2148
2316
  yield Promise.allSettled(
2149
2317
  ev.Records.map((record) => __async(null, null, function* () {
2150
- var _a, _b, _c, _d;
2318
+ var _a, _b, _c, _d, _e;
2151
2319
  const errorContext = {};
2152
2320
  const body = parseSQSBusEvent(record.body);
2153
2321
  const { detail } = body;
@@ -2170,6 +2338,7 @@ var handler2 = (container) => (ev) => __async(null, null, function* () {
2170
2338
  );
2171
2339
  }
2172
2340
  const mutualDataProcessor = (_d = config.mutualDataProcessor) != null ? _d : (() => ({}));
2341
+ const mutualDataSchema = (_e = config.mutual) == null ? void 0 : _e.mutualDataSchema;
2173
2342
  yield mutualRepository.createMutualLock({
2174
2343
  byEntityType,
2175
2344
  byEntityId,
@@ -2205,6 +2374,20 @@ var handler2 = (container) => (ev) => __async(null, null, function* () {
2205
2374
  addedEntityIds,
2206
2375
  (id) => __async(null, null, function* () {
2207
2376
  const entity = yield entityRepository.getEntity(entityType, id);
2377
+ const processedMutualData = mutualDataProcessor(
2378
+ mutualIds,
2379
+ new Mutual(
2380
+ byEntityType,
2381
+ byEntityId,
2382
+ byEntity.data,
2383
+ entityType,
2384
+ id,
2385
+ entity.data,
2386
+ {}
2387
+ ),
2388
+ customContext
2389
+ );
2390
+ const parsedMutualData = mutualDataSchema ? mutualDataSchema.parse(processedMutualData) : processedMutualData;
2208
2391
  yield mutualRepository.createMutual(
2209
2392
  byEntityType,
2210
2393
  byEntityId,
@@ -2212,19 +2395,7 @@ var handler2 = (container) => (ev) => __async(null, null, function* () {
2212
2395
  entityType,
2213
2396
  id,
2214
2397
  entity.data,
2215
- mutualDataProcessor(
2216
- mutualIds,
2217
- new Mutual(
2218
- byEntityType,
2219
- byEntityId,
2220
- byEntity.data,
2221
- entityType,
2222
- id,
2223
- entity.data,
2224
- {}
2225
- ),
2226
- customContext
2227
- ),
2398
+ parsedMutualData,
2228
2399
  {
2229
2400
  ConditionExpression: "attribute_not_exists(#mutualUpdatedAt) OR #mutualUpdatedAt < :publishedAt",
2230
2401
  ExpressionAttributeNames: {
@@ -2261,25 +2432,27 @@ var handler2 = (container) => (ev) => __async(null, null, function* () {
2261
2432
  const updateEntities = yield processEntities(
2262
2433
  toUpdateEntityIds,
2263
2434
  (id) => __async(null, null, function* () {
2435
+ const processedMutualData = mutualDataProcessor(
2436
+ mutualIds,
2437
+ new Mutual(
2438
+ byEntityType,
2439
+ byEntityId,
2440
+ byEntity.data,
2441
+ entityType,
2442
+ id,
2443
+ {},
2444
+ {}
2445
+ ),
2446
+ customContext
2447
+ );
2448
+ const parsedMutualData = mutualDataSchema ? mutualDataSchema.parse(processedMutualData) : processedMutualData;
2264
2449
  yield mutualRepository.updateMutual(
2265
2450
  byEntityType,
2266
2451
  byEntityId,
2267
2452
  entityType,
2268
2453
  id,
2269
2454
  {
2270
- mutualData: mutualDataProcessor(
2271
- mutualIds,
2272
- new Mutual(
2273
- byEntityType,
2274
- byEntityId,
2275
- byEntity.data,
2276
- entityType,
2277
- id,
2278
- {},
2279
- {}
2280
- ),
2281
- customContext
2282
- ),
2455
+ mutualData: parsedMutualData,
2283
2456
  mutualUpdatedAt: publishedAt
2284
2457
  },
2285
2458
  {
@@ -2855,6 +3028,458 @@ var handler5 = (container) => (ev) => __async(null, null, function* () {
2855
3028
  return { batchItemFailures };
2856
3029
  });
2857
3030
 
3031
+ // processors/websocket-processor.ts
3032
+ import {
3033
+ ApiGatewayManagementApiClient,
3034
+ PostToConnectionCommand
3035
+ } from "@aws-sdk/client-apigatewaymanagementapi";
3036
+ import { unmarshall as unmarshall4 } from "@aws-sdk/util-dynamodb";
3037
+ import { ulid as ulid3 } from "ulid";
3038
+ var SUB_ENTITY_TYPE = "SUB#ENTITY#";
3039
+ var SUB_MUTUAL_TYPE = "SUB#MUTUAL#";
3040
+ var SUB_EPHEMERAL = "SUB#EPHEMERAL#";
3041
+ var SUB_FEED = "SUB#FEED#";
3042
+ var getWsEndpoint = () => process.env.WEBSOCKET_MANAGEMENT_ENDPOINT || "";
3043
+ var connect = (container) => (event) => __async(null, null, function* () {
3044
+ var _a, _b, _c, _d;
3045
+ const connectionId = event.requestContext.connectionId;
3046
+ if (!connectionId) {
3047
+ return { statusCode: 400, body: "Missing connection ID" };
3048
+ }
3049
+ const expiresAt = Math.floor(Date.now() / 1e3) + 2 * 60 * 60;
3050
+ const ticket = (_a = event.queryStringParameters) == null ? void 0 : _a.ticket;
3051
+ const token = ((_b = event.queryStringParameters) == null ? void 0 : _b.token) || ((_c = event.headers) == null ? void 0 : _c.authorization) || ((_d = event.headers) == null ? void 0 : _d.Authorization);
3052
+ if (!ticket && !token) {
3053
+ return { statusCode: 401, body: "Unauthorized" };
3054
+ }
3055
+ const wsRepo = container.websocketRepository;
3056
+ try {
3057
+ let entityType;
3058
+ let entityId;
3059
+ let feedTypes;
3060
+ if (ticket) {
3061
+ const ticketData = yield wsRepo.consumeTicket(ticket);
3062
+ if (!ticketData) {
3063
+ return { statusCode: 401, body: "Invalid or expired ticket" };
3064
+ }
3065
+ entityType = ticketData.entityType;
3066
+ entityId = ticketData.entityId;
3067
+ feedTypes = ticketData.feedTypes;
3068
+ } else {
3069
+ entityId = token;
3070
+ }
3071
+ yield wsRepo.createConnection(
3072
+ connectionId,
3073
+ __spreadProps(__spreadValues(__spreadValues({}, entityType && { entityType }), entityId && { entityId }), {
3074
+ connectedAt: (/* @__PURE__ */ new Date()).toISOString()
3075
+ }),
3076
+ expiresAt
3077
+ );
3078
+ if (entityType && entityId && feedTypes) {
3079
+ yield wsRepo.createSubscription(
3080
+ `${SUB_FEED}${entityType}#${entityId}`,
3081
+ connectionId,
3082
+ {
3083
+ subscriptionType: "feed",
3084
+ entityType,
3085
+ entityId,
3086
+ feedTypes,
3087
+ subscribedAt: (/* @__PURE__ */ new Date()).toISOString(),
3088
+ expiresAt
3089
+ }
3090
+ );
3091
+ }
3092
+ return { statusCode: 200, body: "Connected" };
3093
+ } catch (error) {
3094
+ console.error("Error in $connect:", error);
3095
+ return { statusCode: 500, body: "Failed to connect" };
3096
+ }
3097
+ });
3098
+ var disconnect = (container) => (event) => __async(null, null, function* () {
3099
+ const connectionId = event.requestContext.connectionId;
3100
+ if (!connectionId) {
3101
+ return { statusCode: 400, body: "Missing connection ID" };
3102
+ }
3103
+ const wsRepo = container.websocketRepository;
3104
+ try {
3105
+ const subscriptions = yield wsRepo.querySubscriptionsByConnectionId(connectionId);
3106
+ const deletePromises = [];
3107
+ for (const item of subscriptions) {
3108
+ if (item.PK && item.SK) {
3109
+ deletePromises.push(
3110
+ wsRepo.deleteSubscription(item.PK, item.connectionId).catch(
3111
+ (e) => console.warn("Failed to delete subscription on disconnect:", e)
3112
+ )
3113
+ );
3114
+ }
3115
+ }
3116
+ deletePromises.push(wsRepo.deleteConnection(connectionId));
3117
+ yield Promise.all(deletePromises);
3118
+ return { statusCode: 200, body: "Disconnected" };
3119
+ } catch (error) {
3120
+ console.error("Error cleaning up connection:", error);
3121
+ return { statusCode: 500, body: "Failed to disconnect" };
3122
+ }
3123
+ });
3124
+ var $default = (container) => (event) => __async(null, null, function* () {
3125
+ const connectionId = event.requestContext.connectionId;
3126
+ if (!connectionId || !event.body) {
3127
+ return { statusCode: 400, body: "Invalid message" };
3128
+ }
3129
+ let message;
3130
+ try {
3131
+ message = JSON.parse(event.body);
3132
+ } catch (e) {
3133
+ return { statusCode: 400, body: "Invalid JSON" };
3134
+ }
3135
+ const wsRepo = container.websocketRepository;
3136
+ const wsEndpoint = getWsEndpoint();
3137
+ const managementApi = new ApiGatewayManagementApiClient({
3138
+ endpoint: wsEndpoint
3139
+ });
3140
+ try {
3141
+ switch (message.action) {
3142
+ case "subscribe": {
3143
+ const {
3144
+ entityType,
3145
+ byEntityType,
3146
+ byEntityId,
3147
+ mutualEntityType,
3148
+ channel
3149
+ } = message.payload;
3150
+ if (entityType && !byEntityType) {
3151
+ yield wsRepo.createSubscription(
3152
+ `${SUB_ENTITY_TYPE}${entityType}`,
3153
+ connectionId,
3154
+ {
3155
+ subscriptionType: "entity-type",
3156
+ entityType,
3157
+ subscribedAt: (/* @__PURE__ */ new Date()).toISOString()
3158
+ }
3159
+ );
3160
+ } else if (byEntityType && byEntityId && mutualEntityType) {
3161
+ yield wsRepo.createSubscription(
3162
+ `${SUB_MUTUAL_TYPE}${byEntityType}#${byEntityId}#${mutualEntityType}`,
3163
+ connectionId,
3164
+ {
3165
+ subscriptionType: "mutual-type",
3166
+ byEntityType,
3167
+ byEntityId,
3168
+ entityType: mutualEntityType,
3169
+ subscribedAt: (/* @__PURE__ */ new Date()).toISOString()
3170
+ }
3171
+ );
3172
+ } else if (channel) {
3173
+ yield wsRepo.createSubscription(
3174
+ `${SUB_EPHEMERAL}${channel}`,
3175
+ connectionId,
3176
+ {
3177
+ subscriptionType: "ephemeral",
3178
+ channel,
3179
+ subscribedAt: (/* @__PURE__ */ new Date()).toISOString()
3180
+ }
3181
+ );
3182
+ } else {
3183
+ return { statusCode: 400, body: "Invalid subscription parameters" };
3184
+ }
3185
+ const ackMessage = {
3186
+ type: "ack",
3187
+ id: message.id,
3188
+ payload: { action: "subscribe", success: true }
3189
+ };
3190
+ yield managementApi.send(
3191
+ new PostToConnectionCommand({
3192
+ ConnectionId: connectionId,
3193
+ Data: JSON.stringify(ackMessage)
3194
+ })
3195
+ );
3196
+ return { statusCode: 200, body: "Subscribed" };
3197
+ }
3198
+ case "unsubscribe": {
3199
+ const {
3200
+ entityType,
3201
+ byEntityType,
3202
+ byEntityId,
3203
+ mutualEntityType,
3204
+ channel
3205
+ } = message.payload;
3206
+ if (entityType && !byEntityType) {
3207
+ yield wsRepo.deleteSubscription(
3208
+ `${SUB_ENTITY_TYPE}${entityType}`,
3209
+ connectionId
3210
+ );
3211
+ } else if (byEntityType && byEntityId && mutualEntityType) {
3212
+ yield wsRepo.deleteSubscription(
3213
+ `${SUB_MUTUAL_TYPE}${byEntityType}#${byEntityId}#${mutualEntityType}`,
3214
+ connectionId
3215
+ );
3216
+ } else if (channel) {
3217
+ yield wsRepo.deleteSubscription(
3218
+ `${SUB_EPHEMERAL}${channel}`,
3219
+ connectionId
3220
+ );
3221
+ }
3222
+ const ackMessage = {
3223
+ type: "ack",
3224
+ id: message.id,
3225
+ payload: { action: "unsubscribe", success: true }
3226
+ };
3227
+ yield managementApi.send(
3228
+ new PostToConnectionCommand({
3229
+ ConnectionId: connectionId,
3230
+ Data: JSON.stringify(ackMessage)
3231
+ })
3232
+ );
3233
+ return { statusCode: 200, body: "Unsubscribed" };
3234
+ }
3235
+ case "ping": {
3236
+ const pongMessage = {
3237
+ type: "pong",
3238
+ id: message.id,
3239
+ payload: { timestamp: Date.now() }
3240
+ };
3241
+ yield managementApi.send(
3242
+ new PostToConnectionCommand({
3243
+ ConnectionId: connectionId,
3244
+ Data: JSON.stringify(pongMessage)
3245
+ })
3246
+ );
3247
+ return { statusCode: 200, body: "Pong" };
3248
+ }
3249
+ case "ephemeral": {
3250
+ const { channel, data } = message.payload;
3251
+ if (!channel) {
3252
+ return { statusCode: 400, body: "Missing channel" };
3253
+ }
3254
+ const conn = yield wsRepo.getConnection(connectionId);
3255
+ const senderId = conn == null ? void 0 : conn.entityId;
3256
+ const subKey = `${SUB_EPHEMERAL}${channel}`;
3257
+ const ephemeralMessage = {
3258
+ type: "ephemeral",
3259
+ id: ulid3(),
3260
+ payload: { channel, data, senderId }
3261
+ };
3262
+ yield broadcastToSubscribers(
3263
+ managementApi,
3264
+ wsRepo,
3265
+ subKey,
3266
+ ephemeralMessage,
3267
+ connectionId
3268
+ // Exclude sender
3269
+ );
3270
+ return { statusCode: 200, body: "Broadcasted" };
3271
+ }
3272
+ default:
3273
+ return { statusCode: 400, body: "Unknown action" };
3274
+ }
3275
+ } catch (error) {
3276
+ console.error("Error handling message:", error);
3277
+ try {
3278
+ const errorMessage = {
3279
+ type: "error",
3280
+ id: message.id,
3281
+ payload: { message: "Internal server error" }
3282
+ };
3283
+ yield managementApi.send(
3284
+ new PostToConnectionCommand({
3285
+ ConnectionId: connectionId,
3286
+ Data: JSON.stringify(errorMessage)
3287
+ })
3288
+ );
3289
+ } catch (e) {
3290
+ }
3291
+ return { statusCode: 500, body: "Internal server error" };
3292
+ }
3293
+ });
3294
+ var broadcast = (container) => (event) => __async(null, null, function* () {
3295
+ var _a, _b, _c, _d;
3296
+ const wsRepo = container.websocketRepository;
3297
+ const wsEndpoint = getWsEndpoint();
3298
+ const managementApi = new ApiGatewayManagementApiClient({
3299
+ endpoint: wsEndpoint
3300
+ });
3301
+ for (const record of event.Records) {
3302
+ const isInsert = record.eventName === "INSERT";
3303
+ const isModify = record.eventName === "MODIFY";
3304
+ const isRemove = record.eventName === "REMOVE";
3305
+ if (!isInsert && !isModify && !isRemove) continue;
3306
+ const newImage = (_a = record.dynamodb) == null ? void 0 : _a.NewImage;
3307
+ const oldImage = (_b = record.dynamodb) == null ? void 0 : _b.OldImage;
3308
+ const image = newImage || oldImage;
3309
+ if (!image) continue;
3310
+ const pk = ((_c = image.PK) == null ? void 0 : _c.S) || "";
3311
+ const sk = ((_d = image.SK) == null ? void 0 : _d.S) || "";
3312
+ const pkParts = pk.split("#");
3313
+ if (pkParts.length < 2) continue;
3314
+ const firstPart = pkParts[0];
3315
+ if (firstPart === firstPart.toUpperCase() || firstPart.includes(":")) {
3316
+ continue;
3317
+ }
3318
+ const entityType = pkParts[0];
3319
+ const entityId = pkParts[1];
3320
+ const isMutual = !sk.startsWith("#METADATA#") && sk.includes("#");
3321
+ try {
3322
+ if (isMutual) {
3323
+ const skParts = sk.split("#");
3324
+ const mutualEntityType = skParts[0];
3325
+ const byEntityId = entityId;
3326
+ const subKey = `${SUB_MUTUAL_TYPE}${entityType}#${byEntityId}#${mutualEntityType}`;
3327
+ const subscribers = yield wsRepo.querySubscriptionsByKey(subKey);
3328
+ if (subscribers.length) {
3329
+ let eventType;
3330
+ if (isInsert) eventType = "mutual.created";
3331
+ else if (isModify) eventType = "mutual.updated";
3332
+ else eventType = "mutual.deleted";
3333
+ const message = {
3334
+ type: eventType,
3335
+ id: ulid3(),
3336
+ payload: {
3337
+ byEntityType: entityType,
3338
+ byEntityId,
3339
+ mutualEntityType,
3340
+ entityId: skParts[1],
3341
+ data: isRemove ? void 0 : unmarshall4(image)
3342
+ }
3343
+ };
3344
+ yield broadcastToSubscribers(
3345
+ managementApi,
3346
+ wsRepo,
3347
+ subKey,
3348
+ message
3349
+ );
3350
+ }
3351
+ } else {
3352
+ const subKey = `${SUB_ENTITY_TYPE}${entityType}`;
3353
+ const subscribers = yield wsRepo.querySubscriptionsByKey(subKey);
3354
+ if (subscribers.length) {
3355
+ let eventType;
3356
+ if (isInsert) eventType = "entity.created";
3357
+ else if (isModify) eventType = "entity.updated";
3358
+ else eventType = "entity.deleted";
3359
+ const message = {
3360
+ type: eventType,
3361
+ id: ulid3(),
3362
+ payload: {
3363
+ entityType,
3364
+ entityId,
3365
+ data: isRemove ? void 0 : unmarshall4(image)
3366
+ }
3367
+ };
3368
+ yield broadcastToSubscribers(
3369
+ managementApi,
3370
+ wsRepo,
3371
+ subKey,
3372
+ message
3373
+ );
3374
+ }
3375
+ }
3376
+ yield broadcastToFeedSubscribers(
3377
+ managementApi,
3378
+ wsRepo,
3379
+ entityType,
3380
+ entityId,
3381
+ isMutual ? sk.split("#")[0] : entityType,
3382
+ // the changed entity type
3383
+ isMutual ? {
3384
+ type: isInsert ? "mutual.created" : isModify ? "mutual.updated" : "mutual.deleted",
3385
+ id: ulid3(),
3386
+ payload: {
3387
+ byEntityType: entityType,
3388
+ byEntityId: entityId,
3389
+ mutualEntityType: sk.split("#")[0],
3390
+ entityId: sk.split("#")[1],
3391
+ data: isRemove ? void 0 : unmarshall4(image)
3392
+ }
3393
+ } : {
3394
+ type: isInsert ? "entity.created" : isModify ? "entity.updated" : "entity.deleted",
3395
+ id: ulid3(),
3396
+ payload: {
3397
+ entityType,
3398
+ entityId,
3399
+ data: isRemove ? void 0 : unmarshall4(image)
3400
+ }
3401
+ }
3402
+ );
3403
+ } catch (error) {
3404
+ console.error("Error broadcasting:", error);
3405
+ }
3406
+ }
3407
+ });
3408
+ function broadcastToSubscribers(managementApi, wsRepo, subKey, message, excludeConnectionId) {
3409
+ return __async(this, null, function* () {
3410
+ const subscribers = yield wsRepo.querySubscriptionsByKey(subKey);
3411
+ if (!subscribers.length) return;
3412
+ const messageData = JSON.stringify(message);
3413
+ const sends = subscribers.filter((subscriber) => {
3414
+ const id = subscriber.connectionId;
3415
+ return !excludeConnectionId || id !== excludeConnectionId;
3416
+ }).map((subscriber) => __async(null, null, function* () {
3417
+ var _a;
3418
+ try {
3419
+ yield managementApi.send(
3420
+ new PostToConnectionCommand({
3421
+ ConnectionId: subscriber.connectionId,
3422
+ Data: messageData
3423
+ })
3424
+ );
3425
+ } catch (error) {
3426
+ const isGone = (error == null ? void 0 : error.name) === "GoneException" || ((_a = error == null ? void 0 : error.$metadata) == null ? void 0 : _a.httpStatusCode) === 410;
3427
+ if (isGone) {
3428
+ yield wsRepo.deleteSubscription(subKey, subscriber.connectionId).catch(
3429
+ (e) => console.warn("Failed to clean up stale subscription:", e)
3430
+ );
3431
+ }
3432
+ }
3433
+ }));
3434
+ yield Promise.allSettled(sends);
3435
+ });
3436
+ }
3437
+ function broadcastToFeedSubscribers(managementApi, wsRepo, byEntityType, byEntityId, changedEntityType, message) {
3438
+ return __async(this, null, function* () {
3439
+ var _a;
3440
+ const connections = yield wsRepo.queryMutualConnections(
3441
+ byEntityType,
3442
+ byEntityId
3443
+ );
3444
+ if (!connections.length) return;
3445
+ const connectedEntities = new Set(
3446
+ connections.map((c) => `${c.entityType}:${c.entityId}`)
3447
+ );
3448
+ connectedEntities.add(`${byEntityType}:${byEntityId}`);
3449
+ const sentConnections = /* @__PURE__ */ new Set();
3450
+ for (const connEntity of connectedEntities) {
3451
+ const [entityType, entityId] = connEntity.split(":");
3452
+ const feedSubs = yield wsRepo.queryFeedSubscriptions(entityType, entityId);
3453
+ if (!feedSubs.length) continue;
3454
+ for (const feedSub of feedSubs) {
3455
+ const feedTypes = feedSub.feedTypes;
3456
+ const connectionId = feedSub.connectionId;
3457
+ if (feedTypes && !feedTypes.includes(changedEntityType)) continue;
3458
+ if (sentConnections.has(connectionId)) continue;
3459
+ sentConnections.add(connectionId);
3460
+ try {
3461
+ yield managementApi.send(
3462
+ new PostToConnectionCommand({
3463
+ ConnectionId: connectionId,
3464
+ Data: JSON.stringify(message)
3465
+ })
3466
+ );
3467
+ } catch (error) {
3468
+ const isGone = (error == null ? void 0 : error.name) === "GoneException" || ((_a = error == null ? void 0 : error.$metadata) == null ? void 0 : _a.httpStatusCode) === 410;
3469
+ if (isGone) {
3470
+ yield wsRepo.deleteSubscription(
3471
+ `SUB#FEED#${entityType}#${entityId}`,
3472
+ connectionId
3473
+ ).catch(
3474
+ (e) => console.warn("Failed to clean up stale feed subscription:", e)
3475
+ );
3476
+ }
3477
+ }
3478
+ }
3479
+ }
3480
+ });
3481
+ }
3482
+
2858
3483
  // services/DependencyContainer.ts
2859
3484
  import { DynamoDB } from "@aws-sdk/client-dynamodb";
2860
3485
 
@@ -7091,7 +7716,16 @@ var UpdateEntityController = class {
7091
7716
  const accountId = c.req.header("account-id");
7092
7717
  const { entityType, entityId } = c.req.param();
7093
7718
  const body = yield c.req.json();
7094
- const _a = body, { $where: where } = _a, entityPayload = __objRest(_a, ["$where"]);
7719
+ const _a = body, { $condition: condition, $where: where } = _a, entityPayload = __objRest(_a, ["$condition", "$where"]);
7720
+ if (condition !== void 0) {
7721
+ if (typeof condition !== "string" || condition.trim().length === 0) {
7722
+ c.status(httpStatus8.BAD_REQUEST);
7723
+ return c.json({
7724
+ code: "API_VALIDATION_ERROR",
7725
+ message: "$condition must be a non-empty string"
7726
+ });
7727
+ }
7728
+ }
7095
7729
  const errorContext = {
7096
7730
  accountId,
7097
7731
  "req.params": c.req.param(),
@@ -7103,6 +7737,7 @@ var UpdateEntityController = class {
7103
7737
  entityId,
7104
7738
  entityPayload,
7105
7739
  accountId,
7740
+ condition,
7106
7741
  where
7107
7742
  });
7108
7743
  errorContext.entity = entity;
@@ -7121,6 +7756,10 @@ var UpdateEntityController = class {
7121
7756
  c.status(httpStatus8.NOT_FOUND);
7122
7757
  return c.json(__spreadValues({}, err.toJSON()));
7123
7758
  }
7759
+ if (err instanceof StandardError && err.code === StandardErrorCode.INVALID_CONDITION) {
7760
+ c.status(httpStatus8.BAD_REQUEST);
7761
+ return c.json(__spreadValues({}, err.toJSON()));
7762
+ }
7124
7763
  if (err instanceof StandardError && err.code === StandardErrorCode.UNIQUE_VALUE_EXISTS) {
7125
7764
  c.status(httpStatus8.BAD_REQUEST);
7126
7765
  return c.json(__spreadValues({}, err.toJSON()));
@@ -7147,11 +7786,21 @@ var AdjustEntityController = class {
7147
7786
  constructor(entityService) {
7148
7787
  this.entityService = entityService;
7149
7788
  this.controller = createMiddleware9((c) => __async(this, null, function* () {
7150
- var _a;
7789
+ var _b;
7151
7790
  const accountId = c.req.header("account-id") || "";
7152
7791
  const { entityType, entityId } = c.req.param();
7153
7792
  const body = yield c.req.json();
7154
- for (const [key, value] of Object.entries(body)) {
7793
+ const _a = body, { $condition: condition } = _a, adjustments = __objRest(_a, ["$condition"]);
7794
+ if (condition !== void 0) {
7795
+ if (typeof condition !== "string" || condition.trim().length === 0) {
7796
+ c.status(httpStatus9.BAD_REQUEST);
7797
+ return c.json({
7798
+ code: "API_VALIDATION_ERROR",
7799
+ message: "$condition must be a non-empty string"
7800
+ });
7801
+ }
7802
+ }
7803
+ for (const [key, value] of Object.entries(adjustments)) {
7155
7804
  if (typeof value !== "number") {
7156
7805
  c.status(httpStatus9.BAD_REQUEST);
7157
7806
  return c.json({
@@ -7164,8 +7813,9 @@ var AdjustEntityController = class {
7164
7813
  const entity = yield this.entityService.adjustEntity({
7165
7814
  entityType,
7166
7815
  entityId,
7167
- adjustments: body,
7168
- accountId
7816
+ adjustments,
7817
+ accountId,
7818
+ condition
7169
7819
  });
7170
7820
  c.status(httpStatus9.OK);
7171
7821
  return c.json(entity.toJSON());
@@ -7174,7 +7824,11 @@ var AdjustEntityController = class {
7174
7824
  c.status(httpStatus9.NOT_FOUND);
7175
7825
  return c.json(__spreadValues({}, err.toJSON()));
7176
7826
  }
7177
- if ((err == null ? void 0 : err.name) === "ConditionalCheckFailedException" || ((_a = err == null ? void 0 : err.__type) == null ? void 0 : _a.includes("ConditionalCheckFailed"))) {
7827
+ if (err instanceof StandardError && err.code === StandardErrorCode.INVALID_CONDITION) {
7828
+ c.status(httpStatus9.BAD_REQUEST);
7829
+ return c.json(__spreadValues({}, err.toJSON()));
7830
+ }
7831
+ if ((err == null ? void 0 : err.name) === "ConditionalCheckFailedException" || ((_b = err == null ? void 0 : err.__type) == null ? void 0 : _b.includes("ConditionalCheckFailed"))) {
7178
7832
  c.status(httpStatus9.CONFLICT);
7179
7833
  return c.json({
7180
7834
  code: "ADJUSTMENT_CONSTRAINT_VIOLATED",
@@ -7479,6 +8133,9 @@ var UpdateMutualController = class {
7479
8133
  }
7480
8134
  };
7481
8135
 
8136
+ // services/entity.service.ts
8137
+ import { marshall as marshall6 } from "@aws-sdk/util-dynamodb";
8138
+
7482
8139
  // data/utils/build-condition-expression.ts
7483
8140
  import { marshall as marshall5 } from "@aws-sdk/util-dynamodb";
7484
8141
  function buildConditionExpression(where) {
@@ -7540,6 +8197,55 @@ function buildConditionExpression(where) {
7540
8197
  };
7541
8198
  }
7542
8199
 
8200
+ // services/resolve-condition.ts
8201
+ function resolveAdjustmentCondition(_0) {
8202
+ return __async(this, arguments, function* ({
8203
+ conditionName,
8204
+ conditions,
8205
+ adjustments,
8206
+ getEntityData
8207
+ }) {
8208
+ if (!Object.hasOwn(conditions, conditionName)) {
8209
+ throw new StandardError(
8210
+ StandardErrorCode.INVALID_CONDITION,
8211
+ `Unknown adjustment condition: '${conditionName}'`
8212
+ );
8213
+ }
8214
+ const condition = conditions[conditionName];
8215
+ let resolved;
8216
+ if (typeof condition === "function") {
8217
+ const data = yield getEntityData();
8218
+ resolved = condition(data, adjustments);
8219
+ } else {
8220
+ resolved = condition;
8221
+ }
8222
+ return buildConditionExpression(resolved);
8223
+ });
8224
+ }
8225
+ function resolveUpdateCondition(_0) {
8226
+ return __async(this, arguments, function* ({
8227
+ conditionName,
8228
+ conditions,
8229
+ getEntityData
8230
+ }) {
8231
+ if (!Object.hasOwn(conditions, conditionName)) {
8232
+ throw new StandardError(
8233
+ StandardErrorCode.INVALID_CONDITION,
8234
+ `Unknown update condition: '${conditionName}'`
8235
+ );
8236
+ }
8237
+ const condition = conditions[conditionName];
8238
+ let resolved;
8239
+ if (typeof condition === "function") {
8240
+ const data = yield getEntityData();
8241
+ resolved = condition(data);
8242
+ } else {
8243
+ resolved = condition;
8244
+ }
8245
+ return buildConditionExpression(resolved);
8246
+ });
8247
+ }
8248
+
7543
8249
  // services/entity.service.ts
7544
8250
  var EntityService = class {
7545
8251
  constructor(EntityConfig, EmailAuthEnabledEntities, entityRepository, publishEvent2, entityServiceLifeCycle) {
@@ -7595,34 +8301,59 @@ var EntityService = class {
7595
8301
  entityType,
7596
8302
  entityId,
7597
8303
  adjustments,
7598
- accountId
8304
+ accountId,
8305
+ condition
7599
8306
  }) {
7600
- var _a, _b, _c, _d;
7601
- const rawConstraints = (_a = this.EntityConfig[entityType]) == null ? void 0 : _a.adjustmentConstraints;
7602
- let resolvedConstraints = rawConstraints;
7603
- if (rawConstraints) {
8307
+ var _a, _b, _c;
8308
+ const entityConfig = this.EntityConfig[entityType];
8309
+ const adjustmentConditions = entityConfig == null ? void 0 : entityConfig.adjustmentConditions;
8310
+ const rawConstraints = entityConfig == null ? void 0 : entityConfig.adjustmentConstraints;
8311
+ let opts;
8312
+ if (adjustmentConditions) {
8313
+ if (!condition) {
8314
+ throw new StandardError(
8315
+ StandardErrorCode.INVALID_CONDITION,
8316
+ "Entity has adjustmentConditions defined; $condition is required for adjustEntity"
8317
+ );
8318
+ }
8319
+ opts = yield resolveAdjustmentCondition({
8320
+ conditionName: condition,
8321
+ conditions: adjustmentConditions,
8322
+ adjustments,
8323
+ getEntityData: () => __async(this, null, function* () {
8324
+ var _a2;
8325
+ const entity2 = yield this.entityRepository.getEntity(entityType, entityId);
8326
+ return (_a2 = entity2 == null ? void 0 : entity2.data) != null ? _a2 : {};
8327
+ })
8328
+ });
8329
+ } else if (rawConstraints) {
8330
+ console.warn(
8331
+ "[monorise] adjustmentConstraints is deprecated. Use adjustmentConditions instead."
8332
+ );
8333
+ let resolvedConstraints = rawConstraints;
7604
8334
  const hasDynamicFields = Object.values(rawConstraints).some(
7605
8335
  (c) => c.minField || c.maxField
7606
8336
  );
7607
8337
  if (hasDynamicFields) {
7608
8338
  const currentEntity = yield this.entityRepository.getEntity(entityType, entityId);
7609
- const data = (_b = currentEntity == null ? void 0 : currentEntity.data) != null ? _b : {};
8339
+ const data = (_a = currentEntity == null ? void 0 : currentEntity.data) != null ? _a : {};
7610
8340
  resolvedConstraints = {};
7611
8341
  for (const [field, constraint] of Object.entries(rawConstraints)) {
7612
8342
  const resolved = {};
7613
8343
  if (constraint.min !== void 0) resolved.min = constraint.min;
7614
8344
  if (constraint.max !== void 0) resolved.max = constraint.max;
7615
- if (constraint.minField) resolved.min = (_c = data[constraint.minField]) != null ? _c : 0;
7616
- if (constraint.maxField) resolved.max = (_d = data[constraint.maxField]) != null ? _d : Number.MAX_SAFE_INTEGER;
8345
+ if (constraint.minField) resolved.min = (_b = data[constraint.minField]) != null ? _b : 0;
8346
+ if (constraint.maxField) resolved.max = (_c = data[constraint.maxField]) != null ? _c : Number.MAX_SAFE_INTEGER;
7617
8347
  resolvedConstraints[field] = resolved;
7618
8348
  }
7619
8349
  }
8350
+ opts = this.buildLegacyAdjustCondition(adjustments, resolvedConstraints);
7620
8351
  }
7621
8352
  const entity = yield this.entityRepository.adjustEntity(
7622
8353
  entityType,
7623
8354
  entityId,
7624
8355
  adjustments,
7625
- resolvedConstraints
8356
+ opts
7626
8357
  );
7627
8358
  yield this.publishEvent({
7628
8359
  event: EVENT.CORE.ENTITY_UPDATED,
@@ -7641,9 +8372,10 @@ var EntityService = class {
7641
8372
  entityId,
7642
8373
  entityPayload,
7643
8374
  accountId,
8375
+ condition,
7644
8376
  where
7645
8377
  }) {
7646
- var _a, _b;
8378
+ var _a, _b, _c;
7647
8379
  const errorContext = {};
7648
8380
  try {
7649
8381
  const entitySchema = this.EntityConfig[entityType].baseSchema;
@@ -7657,7 +8389,30 @@ var EntityService = class {
7657
8389
  const parsedEntityPayload = entitySchema.parse(entityPayload);
7658
8390
  const parsedMutualPayload = mutualSchema == null ? void 0 : mutualSchema.parse(entityPayload);
7659
8391
  errorContext.parsedMutualPayload = parsedMutualPayload;
7660
- const opts = where && Object.keys(where).length > 0 ? buildConditionExpression(where) : void 0;
8392
+ let opts;
8393
+ if (condition) {
8394
+ const updateConditions = (_b = this.EntityConfig[entityType]) == null ? void 0 : _b.updateConditions;
8395
+ if (!updateConditions) {
8396
+ throw new StandardError(
8397
+ StandardErrorCode.INVALID_CONDITION,
8398
+ `Entity '${entityType}' has no updateConditions defined`
8399
+ );
8400
+ }
8401
+ opts = yield resolveUpdateCondition({
8402
+ conditionName: condition,
8403
+ conditions: updateConditions,
8404
+ getEntityData: () => __async(this, null, function* () {
8405
+ var _a2;
8406
+ const entity2 = yield this.entityRepository.getEntity(entityType, entityId);
8407
+ return (_a2 = entity2 == null ? void 0 : entity2.data) != null ? _a2 : {};
8408
+ })
8409
+ });
8410
+ } else if (where && Object.keys(where).length > 0) {
8411
+ console.warn(
8412
+ "[monorise] $where is deprecated. Use named conditions via $condition instead."
8413
+ );
8414
+ opts = buildConditionExpression(where);
8415
+ }
7661
8416
  const entity = yield this.entityRepository.updateEntity(
7662
8417
  entityType,
7663
8418
  entityId,
@@ -7670,7 +8425,7 @@ var EntityService = class {
7670
8425
  const byEntityId = entityId;
7671
8426
  const publishEventPromises = [];
7672
8427
  for (const [fieldKey, config] of Object.entries(
7673
- ((_b = this.EntityConfig[entityType].mutual) == null ? void 0 : _b.mutualFields) || {}
8428
+ ((_c = this.EntityConfig[entityType].mutual) == null ? void 0 : _c.mutualFields) || {}
7674
8429
  )) {
7675
8430
  const toMutualIds = config.toMutualIds;
7676
8431
  const mutualPayload = parsedMutualPayload[fieldKey];
@@ -7726,12 +8481,42 @@ var EntityService = class {
7726
8481
  });
7727
8482
  });
7728
8483
  }
8484
+ /** @deprecated Converts legacy adjustmentConstraints to condition expression opts. */
8485
+ buildLegacyAdjustCondition(adjustments, constraints) {
8486
+ const conditionParts = [];
8487
+ const names = { "#data": "data" };
8488
+ const values = {};
8489
+ for (const [field, constraint] of Object.entries(constraints)) {
8490
+ const delta = adjustments[field];
8491
+ if (delta === void 0) continue;
8492
+ const namePlaceholder = `#where_${field}`;
8493
+ names[namePlaceholder] = field;
8494
+ const fieldRef = `#data.${namePlaceholder}`;
8495
+ if (constraint.min !== void 0 && delta < 0) {
8496
+ const valKey = `:where_${field}_min_threshold`;
8497
+ conditionParts.push(`${fieldRef} >= ${valKey}`);
8498
+ values[valKey] = constraint.min - delta;
8499
+ }
8500
+ if (constraint.max !== void 0 && delta > 0) {
8501
+ const valKey = `:where_${field}_max_threshold`;
8502
+ conditionParts.push(`${fieldRef} <= ${valKey}`);
8503
+ values[valKey] = constraint.max - delta;
8504
+ }
8505
+ }
8506
+ if (conditionParts.length === 0) return void 0;
8507
+ return {
8508
+ ConditionExpression: conditionParts.join(" AND "),
8509
+ ExpressionAttributeNames: names,
8510
+ ExpressionAttributeValues: marshall6(values)
8511
+ };
8512
+ }
7729
8513
  };
7730
8514
 
7731
8515
  // services/mutual.service.ts
7732
- import { ulid as ulid3 } from "ulid";
8516
+ import { ulid as ulid4 } from "ulid";
7733
8517
  var MutualService = class {
7734
- constructor(entityRepository, mutualRepository, publishEvent2, ddbUtils, entityServiceLifeCycle) {
8518
+ constructor(EntityConfig, entityRepository, mutualRepository, publishEvent2, ddbUtils, entityServiceLifeCycle) {
8519
+ this.EntityConfig = EntityConfig;
7735
8520
  this.entityRepository = entityRepository;
7736
8521
  this.mutualRepository = mutualRepository;
7737
8522
  this.publishEvent = publishEvent2;
@@ -7746,6 +8531,7 @@ var MutualService = class {
7746
8531
  accountId,
7747
8532
  options = {}
7748
8533
  }) {
8534
+ var _a;
7749
8535
  const {
7750
8536
  ensureEntityStrongConsistentWrite = false,
7751
8537
  asEntity,
@@ -7766,7 +8552,7 @@ var MutualService = class {
7766
8552
  options
7767
8553
  }
7768
8554
  };
7769
- const schema = external_exports.record(external_exports.string(), external_exports.any());
8555
+ const schema = (_a = this.getMutualDataSchema(byEntityType, entityType)) != null ? _a : external_exports.record(external_exports.string(), external_exports.any());
7770
8556
  const parsedMutualPayload = schema.parse(mutualPayload);
7771
8557
  const [{ data: byEntityData }, { data: entityData }] = yield Promise.all([
7772
8558
  this.entityRepository.getEntity(byEntityType, byEntityId),
@@ -7789,7 +8575,7 @@ var MutualService = class {
7789
8575
  entityId,
7790
8576
  entityData,
7791
8577
  parsedMutualPayload,
7792
- mutualId || ulid3(),
8578
+ mutualId || ulid4(),
7793
8579
  currentDatetime,
7794
8580
  currentDatetime,
7795
8581
  currentDatetime
@@ -7869,7 +8655,8 @@ var MutualService = class {
7869
8655
  accountId,
7870
8656
  options
7871
8657
  }) {
7872
- const schema = external_exports.record(external_exports.string(), external_exports.any());
8658
+ var _a;
8659
+ const schema = (_a = this.getMutualDataSchema(byEntityType, entityType)) != null ? _a : external_exports.record(external_exports.string(), external_exports.any());
7873
8660
  const parsedMutualPayload = schema.parse(mutualPayload);
7874
8661
  const mutual = yield this.mutualRepository.updateMutual(
7875
8662
  byEntityType,
@@ -7918,6 +8705,22 @@ var MutualService = class {
7918
8705
  return mutual;
7919
8706
  });
7920
8707
  }
8708
+ getMutualDataSchema(byEntityType, entityType) {
8709
+ var _a, _b, _c;
8710
+ for (const [from, to] of [
8711
+ [byEntityType, entityType],
8712
+ [entityType, byEntityType]
8713
+ ]) {
8714
+ const mutualFields = (_b = (_a = this.EntityConfig[from]) == null ? void 0 : _a.mutual) == null ? void 0 : _b.mutualFields;
8715
+ if (!mutualFields) continue;
8716
+ for (const config of Object.values(mutualFields)) {
8717
+ if (config.entityType === to && ((_c = config.mutual) == null ? void 0 : _c.mutualDataSchema)) {
8718
+ return config.mutual.mutualDataSchema;
8719
+ }
8720
+ }
8721
+ }
8722
+ return void 0;
8723
+ }
7921
8724
  };
7922
8725
 
7923
8726
  // controllers/tag/list-tags.controller.ts
@@ -7967,6 +8770,116 @@ var ListTagsController = class {
7967
8770
  }
7968
8771
  };
7969
8772
 
8773
+ // controllers/transaction/execute-transaction.controller.ts
8774
+ import { createMiddleware as createMiddleware17 } from "hono/factory";
8775
+ import httpStatus15 from "http-status";
8776
+ var ExecuteTransactionController = class {
8777
+ constructor(transactionService) {
8778
+ this.transactionService = transactionService;
8779
+ // biome-ignore lint/suspicious/noExplicitAny: Hono createMiddleware requires consistent return types
8780
+ this.controller = createMiddleware17((c) => __async(this, null, function* () {
8781
+ var _a;
8782
+ const accountId = c.req.header("account-id") || "";
8783
+ const body = yield c.req.json();
8784
+ if (!body.operations || !Array.isArray(body.operations)) {
8785
+ c.status(httpStatus15.BAD_REQUEST);
8786
+ return c.json({
8787
+ code: "API_VALIDATION_ERROR",
8788
+ message: 'Request body must contain an "operations" array'
8789
+ });
8790
+ }
8791
+ try {
8792
+ const result = yield this.transactionService.executeTransaction(
8793
+ body.operations,
8794
+ accountId
8795
+ );
8796
+ c.status(httpStatus15.OK);
8797
+ return c.json(result);
8798
+ } catch (err) {
8799
+ if (((_a = err.constructor) == null ? void 0 : _a.name) === "ZodError") {
8800
+ c.status(httpStatus15.BAD_REQUEST);
8801
+ return c.json({
8802
+ code: "API_VALIDATION_ERROR",
8803
+ message: "Validation failed",
8804
+ details: err.flatten()
8805
+ });
8806
+ }
8807
+ if (err instanceof StandardError) {
8808
+ const code = err.code;
8809
+ if (code === StandardErrorCode.TRANSACTION_EMPTY || code === StandardErrorCode.TRANSACTION_ITEM_LIMIT_EXCEEDED || code === StandardErrorCode.TRANSACTION_UNIQUE_FIELD_UPDATE || code === StandardErrorCode.INVALID_ENTITY_TYPE || code === StandardErrorCode.INVALID_CONDITION || code === StandardErrorCode.INVALID_UNIQUE_VALUE_TYPE) {
8810
+ c.status(httpStatus15.BAD_REQUEST);
8811
+ return c.json(__spreadValues({}, err.toJSON()));
8812
+ }
8813
+ if (code === StandardErrorCode.TRANSACTION_FAILED || code === StandardErrorCode.CONDITIONAL_CHECK_FAILED || code === StandardErrorCode.UNIQUE_VALUE_EXISTS) {
8814
+ c.status(httpStatus15.CONFLICT);
8815
+ return c.json(__spreadValues({}, err.toJSON()));
8816
+ }
8817
+ }
8818
+ throw err;
8819
+ }
8820
+ }));
8821
+ }
8822
+ };
8823
+
8824
+ // controllers/ws/create-ticket.controller.ts
8825
+ import { createMiddleware as createMiddleware18 } from "hono/factory";
8826
+ import { ulid as ulid5 } from "ulid";
8827
+ var TICKET_TTL_SECONDS = 30 * 60;
8828
+ var CreateTicketController = class {
8829
+ constructor(container) {
8830
+ this.container = container;
8831
+ this.controller = createMiddleware18((c) => __async(this, null, function* () {
8832
+ var _a;
8833
+ const { entityType, entityId } = c.req.param();
8834
+ let feedTypes;
8835
+ try {
8836
+ const body = yield c.req.json();
8837
+ feedTypes = body.feedTypes;
8838
+ } catch (e) {
8839
+ }
8840
+ if (!feedTypes || feedTypes.length === 0) {
8841
+ const allConfigs = this.container.config.EntityConfig;
8842
+ const visited = /* @__PURE__ */ new Set();
8843
+ const queue = [entityType];
8844
+ while (queue.length > 0) {
8845
+ const current = queue.shift();
8846
+ if (!current) continue;
8847
+ if (visited.has(current)) continue;
8848
+ visited.add(current);
8849
+ const config = allConfigs[current];
8850
+ if ((_a = config == null ? void 0 : config.mutual) == null ? void 0 : _a.mutualFields) {
8851
+ for (const field of Object.values(
8852
+ config.mutual.mutualFields
8853
+ )) {
8854
+ if (!visited.has(field.entityType)) {
8855
+ queue.push(field.entityType);
8856
+ }
8857
+ }
8858
+ }
8859
+ }
8860
+ visited.delete(entityType);
8861
+ feedTypes = Array.from(visited);
8862
+ }
8863
+ const ticket = ulid5();
8864
+ const now = Math.floor(Date.now() / 1e3);
8865
+ const expiresAt = now + TICKET_TTL_SECONDS;
8866
+ yield this.container.websocketRepository.createTicket(
8867
+ ticket,
8868
+ entityType,
8869
+ entityId,
8870
+ feedTypes,
8871
+ expiresAt
8872
+ );
8873
+ const wsEndpoint = process.env.WEBSOCKET_URL || "";
8874
+ return c.json({
8875
+ ticket,
8876
+ wsUrl: wsEndpoint,
8877
+ expiresIn: TICKET_TTL_SECONDS
8878
+ });
8879
+ }));
8880
+ }
8881
+ };
8882
+
7970
8883
  // services/entity-service-lifecycle.ts
7971
8884
  var EntityServiceLifeCycle = class {
7972
8885
  constructor(EntityConfig, publishEvent2, eventUtils) {
@@ -7999,6 +8912,435 @@ var EntityServiceLifeCycle = class {
7999
8912
  }
8000
8913
  };
8001
8914
 
8915
+ // services/transaction.service.ts
8916
+ import { TransactionCanceledException as TransactionCanceledException5 } from "@aws-sdk/client-dynamodb";
8917
+ import { ulid as ulid6 } from "ulid";
8918
+ var MAX_TRANSACTION_ITEMS = 100;
8919
+ var TransactionService = class {
8920
+ constructor(EntityConfig, EmailAuthEnabledEntities, entityRepository, dynamodbClient, publishEvent2, entityServiceLifeCycle, eventUtils) {
8921
+ this.EntityConfig = EntityConfig;
8922
+ this.EmailAuthEnabledEntities = EmailAuthEnabledEntities;
8923
+ this.entityRepository = entityRepository;
8924
+ this.dynamodbClient = dynamodbClient;
8925
+ this.publishEvent = publishEvent2;
8926
+ this.entityServiceLifeCycle = entityServiceLifeCycle;
8927
+ this.eventUtils = eventUtils;
8928
+ this.executeTransaction = (operations, accountId) => __async(this, null, function* () {
8929
+ var _a;
8930
+ if (!operations || operations.length === 0) {
8931
+ throw new StandardError(
8932
+ StandardErrorCode.TRANSACTION_EMPTY,
8933
+ "Transaction must contain at least one operation"
8934
+ );
8935
+ }
8936
+ const allTransactItems = [];
8937
+ const pendingEvents = [];
8938
+ const resultEntries = [];
8939
+ for (const op of operations) {
8940
+ switch (op.operation) {
8941
+ case "createEntity": {
8942
+ const { items, entity } = yield this.buildCreateItems(op);
8943
+ allTransactItems.push(...items);
8944
+ pendingEvents.push(
8945
+ ...this.collectCreateEvents(
8946
+ entity,
8947
+ op.payload,
8948
+ accountId
8949
+ )
8950
+ );
8951
+ resultEntries.push({
8952
+ operation: "createEntity",
8953
+ entityType: op.entityType,
8954
+ entityId: entity.entityId,
8955
+ data: entity.data
8956
+ });
8957
+ break;
8958
+ }
8959
+ case "updateEntity": {
8960
+ const { item, updatedAt } = yield this.buildUpdateItem(op);
8961
+ allTransactItems.push(item);
8962
+ pendingEvents.push(
8963
+ ...this.collectUpdateEvents(
8964
+ op,
8965
+ updatedAt,
8966
+ accountId
8967
+ )
8968
+ );
8969
+ resultEntries.push({
8970
+ operation: "updateEntity",
8971
+ entityType: op.entityType,
8972
+ entityId: op.entityId
8973
+ });
8974
+ break;
8975
+ }
8976
+ case "adjustEntity": {
8977
+ const { item, updatedAt } = yield this.buildAdjustItem(op);
8978
+ allTransactItems.push(item);
8979
+ pendingEvents.push({
8980
+ event: EVENT.CORE.ENTITY_UPDATED,
8981
+ payload: {
8982
+ entityType: op.entityType,
8983
+ entityId: op.entityId,
8984
+ updatedByAccountId: accountId,
8985
+ publishedAt: updatedAt
8986
+ }
8987
+ });
8988
+ resultEntries.push({
8989
+ operation: "adjustEntity",
8990
+ entityType: op.entityType,
8991
+ entityId: op.entityId
8992
+ });
8993
+ break;
8994
+ }
8995
+ case "deleteEntity": {
8996
+ const item = this.buildDeleteItem(op);
8997
+ allTransactItems.push(item);
8998
+ pendingEvents.push({
8999
+ event: EVENT.CORE.ENTITY_DELETED,
9000
+ payload: {
9001
+ entityType: op.entityType,
9002
+ entityId: op.entityId,
9003
+ deletedByAccountId: accountId
9004
+ }
9005
+ });
9006
+ resultEntries.push({
9007
+ operation: "deleteEntity",
9008
+ entityType: op.entityType,
9009
+ entityId: op.entityId
9010
+ });
9011
+ break;
9012
+ }
9013
+ default:
9014
+ throw new StandardError(
9015
+ StandardErrorCode.INVALID_ENTITY_TYPE,
9016
+ `Unknown operation: '${op.operation}'`
9017
+ );
9018
+ }
9019
+ }
9020
+ if (allTransactItems.length > MAX_TRANSACTION_ITEMS) {
9021
+ throw new StandardError(
9022
+ StandardErrorCode.TRANSACTION_ITEM_LIMIT_EXCEEDED,
9023
+ `Transaction contains ${allTransactItems.length} items, exceeds limit of ${MAX_TRANSACTION_ITEMS}`
9024
+ );
9025
+ }
9026
+ try {
9027
+ yield this.dynamodbClient.transactWriteItems({
9028
+ TransactItems: allTransactItems
9029
+ });
9030
+ } catch (err) {
9031
+ if (err instanceof TransactionCanceledException5) {
9032
+ throw new StandardError(
9033
+ StandardErrorCode.TRANSACTION_FAILED,
9034
+ "Transaction failed",
9035
+ err,
9036
+ {
9037
+ reasons: (_a = err.CancellationReasons) == null ? void 0 : _a.map((r, i) => ({
9038
+ index: i,
9039
+ code: r.Code,
9040
+ message: r.Message
9041
+ }))
9042
+ }
9043
+ );
9044
+ }
9045
+ throw err;
9046
+ }
9047
+ const readPromises = resultEntries.map((entry) => __async(this, null, function* () {
9048
+ if ((entry.operation === "updateEntity" || entry.operation === "adjustEntity") && !entry.data) {
9049
+ try {
9050
+ const entity = yield this.entityRepository.getEntity(
9051
+ entry.entityType,
9052
+ entry.entityId
9053
+ );
9054
+ entry.data = entity.data;
9055
+ } catch (e) {
9056
+ }
9057
+ }
9058
+ }));
9059
+ yield Promise.all(readPromises);
9060
+ yield Promise.allSettled(pendingEvents.map((ev) => this.publishEvent(ev)));
9061
+ return { results: resultEntries };
9062
+ });
9063
+ }
9064
+ buildCreateItems(op) {
9065
+ return __async(this, null, function* () {
9066
+ const config = this.EntityConfig[op.entityType];
9067
+ if (!config) {
9068
+ throw new StandardError(
9069
+ StandardErrorCode.INVALID_ENTITY_TYPE,
9070
+ `Unknown entity type: '${op.entityType}'`
9071
+ );
9072
+ }
9073
+ const entitySchema = config.createSchema || config.baseSchema;
9074
+ if (!entitySchema) {
9075
+ throw new StandardError(
9076
+ StandardErrorCode.INVALID_ENTITY_TYPE,
9077
+ `No schema defined for entity type: '${op.entityType}'`
9078
+ );
9079
+ }
9080
+ if (config.finalSchema) {
9081
+ config.finalSchema.parse(op.payload);
9082
+ }
9083
+ const parsedPayload = entitySchema.parse(
9084
+ op.payload
9085
+ );
9086
+ const currentDatetime = /* @__PURE__ */ new Date();
9087
+ const entity = new Entity(
9088
+ op.entityType,
9089
+ op.entityId || ulid6(),
9090
+ parsedPayload,
9091
+ currentDatetime,
9092
+ currentDatetime
9093
+ );
9094
+ const uniqueFields = config.uniqueFields || [];
9095
+ const uniqueFieldValues = {};
9096
+ for (const field of uniqueFields) {
9097
+ if (!(field in parsedPayload)) continue;
9098
+ const value = parsedPayload[field];
9099
+ if (typeof value !== "string") {
9100
+ throw new StandardError(
9101
+ StandardErrorCode.INVALID_UNIQUE_VALUE_TYPE,
9102
+ `Invalid type. ${field} is not a 'string'.`
9103
+ );
9104
+ }
9105
+ uniqueFieldValues[field] = value;
9106
+ }
9107
+ const items = this.entityRepository.createEntityTransactItems(entity, {
9108
+ uniqueFieldValues
9109
+ });
9110
+ return { items, entity };
9111
+ });
9112
+ }
9113
+ buildUpdateItem(op) {
9114
+ return __async(this, null, function* () {
9115
+ const config = this.EntityConfig[op.entityType];
9116
+ if (!config) {
9117
+ throw new StandardError(
9118
+ StandardErrorCode.INVALID_ENTITY_TYPE,
9119
+ `Unknown entity type: '${op.entityType}'`
9120
+ );
9121
+ }
9122
+ const uniqueFields = config.uniqueFields || [];
9123
+ for (const field of uniqueFields) {
9124
+ if (field in op.payload) {
9125
+ throw new StandardError(
9126
+ StandardErrorCode.TRANSACTION_UNIQUE_FIELD_UPDATE,
9127
+ `Cannot update unique field '${field}' within a transaction. Use a standalone updateEntity call instead.`
9128
+ );
9129
+ }
9130
+ }
9131
+ const entitySchema = config.baseSchema;
9132
+ if (!entitySchema) {
9133
+ throw new StandardError(
9134
+ StandardErrorCode.INVALID_ENTITY_TYPE,
9135
+ `No schema defined for entity type: '${op.entityType}'`
9136
+ );
9137
+ }
9138
+ const parsedPayload = entitySchema.partial().parse(op.payload);
9139
+ const currentDatetime = (/* @__PURE__ */ new Date()).toISOString();
9140
+ const toUpdateExpressions = this.entityRepository.toUpdate({
9141
+ updatedAt: currentDatetime,
9142
+ data: parsedPayload
9143
+ });
9144
+ let conditionOpts;
9145
+ if (op.condition) {
9146
+ const updateConditions = config.updateConditions;
9147
+ if (!updateConditions) {
9148
+ throw new StandardError(
9149
+ StandardErrorCode.INVALID_CONDITION,
9150
+ `Entity '${op.entityType}' has no updateConditions defined`
9151
+ );
9152
+ }
9153
+ conditionOpts = yield resolveUpdateCondition({
9154
+ conditionName: op.condition,
9155
+ conditions: updateConditions,
9156
+ getEntityData: () => __async(this, null, function* () {
9157
+ var _a;
9158
+ const entity2 = yield this.entityRepository.getEntity(
9159
+ op.entityType,
9160
+ op.entityId
9161
+ );
9162
+ return (_a = entity2 == null ? void 0 : entity2.data) != null ? _a : {};
9163
+ })
9164
+ });
9165
+ }
9166
+ const entity = new Entity(op.entityType, op.entityId);
9167
+ const item = {
9168
+ Update: {
9169
+ TableName: this.entityRepository.TABLE_NAME,
9170
+ Key: entity.keys(),
9171
+ ConditionExpression: (conditionOpts == null ? void 0 : conditionOpts.ConditionExpression) || "attribute_exists(PK)",
9172
+ UpdateExpression: toUpdateExpressions.UpdateExpression,
9173
+ ExpressionAttributeNames: __spreadValues(__spreadValues({}, toUpdateExpressions.ExpressionAttributeNames), conditionOpts == null ? void 0 : conditionOpts.ExpressionAttributeNames),
9174
+ ExpressionAttributeValues: __spreadValues(__spreadValues({}, toUpdateExpressions.ExpressionAttributeValues), conditionOpts == null ? void 0 : conditionOpts.ExpressionAttributeValues)
9175
+ }
9176
+ };
9177
+ return { item, updatedAt: currentDatetime };
9178
+ });
9179
+ }
9180
+ buildAdjustItem(op) {
9181
+ return __async(this, null, function* () {
9182
+ const config = this.EntityConfig[op.entityType];
9183
+ if (!config) {
9184
+ throw new StandardError(
9185
+ StandardErrorCode.INVALID_ENTITY_TYPE,
9186
+ `Unknown entity type: '${op.entityType}'`
9187
+ );
9188
+ }
9189
+ for (const [key, value] of Object.entries(op.adjustments)) {
9190
+ if (typeof value !== "number" || !Number.isFinite(value)) {
9191
+ throw new StandardError(
9192
+ StandardErrorCode.INVALID_ENTITY_TYPE,
9193
+ `Adjustment field "${key}" must be a finite number`
9194
+ );
9195
+ }
9196
+ }
9197
+ const {
9198
+ UpdateExpression,
9199
+ ExpressionAttributeNames,
9200
+ ExpressionAttributeValues
9201
+ } = this.entityRepository.toAdjustUpdate(op.adjustments);
9202
+ const currentDatetime = (/* @__PURE__ */ new Date()).toISOString();
9203
+ ExpressionAttributeNames["#updatedAt"] = "updatedAt";
9204
+ ExpressionAttributeValues[":updatedAt"] = { S: currentDatetime };
9205
+ const fullUpdateExpression = `${UpdateExpression}, #updatedAt = :updatedAt`;
9206
+ let conditionOpts;
9207
+ const adjustmentConditions = config.adjustmentConditions;
9208
+ if (adjustmentConditions) {
9209
+ if (!op.condition) {
9210
+ throw new StandardError(
9211
+ StandardErrorCode.INVALID_CONDITION,
9212
+ `Entity '${op.entityType}' has adjustmentConditions defined; condition is required`
9213
+ );
9214
+ }
9215
+ conditionOpts = yield resolveAdjustmentCondition({
9216
+ conditionName: op.condition,
9217
+ conditions: adjustmentConditions,
9218
+ adjustments: op.adjustments,
9219
+ getEntityData: () => __async(this, null, function* () {
9220
+ var _a;
9221
+ const entity2 = yield this.entityRepository.getEntity(
9222
+ op.entityType,
9223
+ op.entityId
9224
+ );
9225
+ return (_a = entity2 == null ? void 0 : entity2.data) != null ? _a : {};
9226
+ })
9227
+ });
9228
+ }
9229
+ const entity = new Entity(op.entityType, op.entityId);
9230
+ const item = {
9231
+ Update: {
9232
+ TableName: this.entityRepository.TABLE_NAME,
9233
+ Key: entity.keys(),
9234
+ UpdateExpression: fullUpdateExpression,
9235
+ ConditionExpression: (conditionOpts == null ? void 0 : conditionOpts.ConditionExpression) || "attribute_exists(PK)",
9236
+ ExpressionAttributeNames: __spreadValues(__spreadValues({}, ExpressionAttributeNames), conditionOpts == null ? void 0 : conditionOpts.ExpressionAttributeNames),
9237
+ ExpressionAttributeValues: __spreadValues(__spreadValues({}, ExpressionAttributeValues), conditionOpts == null ? void 0 : conditionOpts.ExpressionAttributeValues)
9238
+ }
9239
+ };
9240
+ return { item, updatedAt: currentDatetime };
9241
+ });
9242
+ }
9243
+ buildDeleteItem(op) {
9244
+ const config = this.EntityConfig[op.entityType];
9245
+ if (!config) {
9246
+ throw new StandardError(
9247
+ StandardErrorCode.INVALID_ENTITY_TYPE,
9248
+ `Unknown entity type: '${op.entityType}'`
9249
+ );
9250
+ }
9251
+ const entity = new Entity(op.entityType, op.entityId);
9252
+ return {
9253
+ Delete: {
9254
+ TableName: this.entityRepository.TABLE_NAME,
9255
+ Key: entity.keys(),
9256
+ ConditionExpression: "attribute_exists(PK)"
9257
+ }
9258
+ };
9259
+ }
9260
+ collectCreateEvents(entity, payload, accountId) {
9261
+ var _a, _b;
9262
+ const events = [];
9263
+ const publishedAt = entity.updatedAt || (/* @__PURE__ */ new Date()).toISOString();
9264
+ const config = this.EntityConfig[entity.entityType];
9265
+ const mutualSchema = (_a = config == null ? void 0 : config.mutual) == null ? void 0 : _a.mutualSchema;
9266
+ if (mutualSchema) {
9267
+ const parsedMutualPayload = mutualSchema.parse(payload);
9268
+ if (parsedMutualPayload) {
9269
+ for (const [fieldKey, fieldConfig] of Object.entries(
9270
+ ((_b = config.mutual) == null ? void 0 : _b.mutualFields) || {}
9271
+ )) {
9272
+ const toMutualIds = fieldConfig.toMutualIds;
9273
+ const mutualPayload = parsedMutualPayload[fieldKey];
9274
+ if (!mutualPayload) continue;
9275
+ events.push({
9276
+ event: EVENT.CORE.ENTITY_MUTUAL_TO_CREATE,
9277
+ payload: {
9278
+ byEntityType: entity.entityType,
9279
+ byEntityId: entity.entityId,
9280
+ entityType: fieldConfig.entityType,
9281
+ field: fieldKey,
9282
+ mutualIds: toMutualIds ? toMutualIds(mutualPayload) : mutualPayload,
9283
+ customContext: toMutualIds ? mutualPayload : {},
9284
+ publishedAt
9285
+ }
9286
+ });
9287
+ }
9288
+ }
9289
+ }
9290
+ events.push({
9291
+ event: EVENT.CORE.ENTITY_CREATED,
9292
+ payload: {
9293
+ entityType: entity.entityType,
9294
+ entityId: entity.entityId,
9295
+ data: entity.data,
9296
+ createdByAccountId: accountId,
9297
+ publishedAt
9298
+ }
9299
+ });
9300
+ return events;
9301
+ }
9302
+ collectUpdateEvents(op, updatedAt, accountId) {
9303
+ var _a, _b;
9304
+ const events = [];
9305
+ const config = this.EntityConfig[op.entityType];
9306
+ const mutualSchema = (_a = config == null ? void 0 : config.mutual) == null ? void 0 : _a.mutualSchema;
9307
+ if (mutualSchema) {
9308
+ const parsedMutualPayload = mutualSchema.parse(op.payload);
9309
+ if (parsedMutualPayload) {
9310
+ for (const [fieldKey, fieldConfig] of Object.entries(
9311
+ ((_b = config.mutual) == null ? void 0 : _b.mutualFields) || {}
9312
+ )) {
9313
+ const toMutualIds = fieldConfig.toMutualIds;
9314
+ const mutualPayload = parsedMutualPayload[fieldKey];
9315
+ if (!mutualPayload) continue;
9316
+ events.push({
9317
+ event: EVENT.CORE.ENTITY_MUTUAL_TO_UPDATE,
9318
+ payload: {
9319
+ byEntityType: op.entityType,
9320
+ byEntityId: op.entityId,
9321
+ entityType: fieldConfig.entityType,
9322
+ field: fieldKey,
9323
+ mutualIds: toMutualIds ? toMutualIds(mutualPayload) : mutualPayload,
9324
+ customContext: toMutualIds ? mutualPayload : {},
9325
+ publishedAt: updatedAt
9326
+ }
9327
+ });
9328
+ }
9329
+ }
9330
+ }
9331
+ events.push({
9332
+ event: EVENT.CORE.ENTITY_UPDATED,
9333
+ payload: {
9334
+ entityType: op.entityType,
9335
+ entityId: op.entityId,
9336
+ updatedByAccountId: accountId,
9337
+ publishedAt: updatedAt
9338
+ }
9339
+ });
9340
+ return events;
9341
+ }
9342
+ };
9343
+
8002
9344
  // services/DependencyContainer.ts
8003
9345
  var DependencyContainer = class {
8004
9346
  constructor(config) {
@@ -8077,6 +9419,7 @@ var DependencyContainer = class {
8077
9419
  get mutualService() {
8078
9420
  return this.createCachedInstance(
8079
9421
  MutualService,
9422
+ this.config.EntityConfig,
8080
9423
  this.entityRepository,
8081
9424
  this.mutualRepository,
8082
9425
  this.publishEvent,
@@ -8091,6 +9434,13 @@ var DependencyContainer = class {
8091
9434
  this.dynamodbClient
8092
9435
  );
8093
9436
  }
9437
+ get websocketRepository() {
9438
+ return this.createCachedInstance(
9439
+ WebSocketRepository,
9440
+ this.coreTable,
9441
+ this.dynamodbClient
9442
+ );
9443
+ }
8094
9444
  get getEntityController() {
8095
9445
  return this.createCachedInstance(
8096
9446
  GetEntityController,
@@ -8174,6 +9524,62 @@ var DependencyContainer = class {
8174
9524
  get listTagsController() {
8175
9525
  return this.createCachedInstance(ListTagsController, this.tagRepository);
8176
9526
  }
9527
+ get transactionService() {
9528
+ return this.createCachedInstance(
9529
+ TransactionService,
9530
+ this.config.EntityConfig,
9531
+ this.config.EmailAuthEnabledEntities,
9532
+ this.entityRepository,
9533
+ this.dynamodbClient,
9534
+ this.publishEvent,
9535
+ this.entityServiceLifeCycle,
9536
+ this.eventUtils
9537
+ );
9538
+ }
9539
+ get executeTransactionController() {
9540
+ return this.createCachedInstance(
9541
+ ExecuteTransactionController,
9542
+ this.transactionService
9543
+ );
9544
+ }
9545
+ get createTicketController() {
9546
+ return this.createCachedInstance(CreateTicketController, this);
9547
+ }
9548
+ };
9549
+
9550
+ // helpers/transactional.ts
9551
+ var transactional = {
9552
+ createEntity: (entityType, payload) => {
9553
+ const _a = payload, { entityId } = _a, rest = __objRest(_a, ["entityId"]);
9554
+ return __spreadValues({
9555
+ operation: "createEntity",
9556
+ entityType,
9557
+ payload: rest
9558
+ }, entityId && { entityId });
9559
+ },
9560
+ updateEntity: (entityType, entityId, payload) => {
9561
+ const _a = payload, { $condition } = _a, rest = __objRest(_a, ["$condition"]);
9562
+ return __spreadValues({
9563
+ operation: "updateEntity",
9564
+ entityType,
9565
+ entityId,
9566
+ payload: rest
9567
+ }, $condition && { condition: $condition });
9568
+ },
9569
+ adjustEntity: (entityType, entityId, adjustments) => {
9570
+ const _a = adjustments, { $condition } = _a, rest = __objRest(_a, ["$condition"]);
9571
+ return __spreadValues({
9572
+ operation: "adjustEntity",
9573
+ entityType,
9574
+ entityId,
9575
+ adjustments: rest
9576
+ }, $condition && { condition: $condition });
9577
+ },
9578
+ deleteEntity: (entityType, entityId) => ({
9579
+ operation: "deleteEntity",
9580
+ entityType,
9581
+ entityId
9582
+ })
8177
9583
  };
8178
9584
 
8179
9585
  // index.ts
@@ -8189,6 +9595,10 @@ var CoreFactory = class {
8189
9595
  this.prejoinProcessor = handler3(dependencyContainer);
8190
9596
  this.tagProcessor = handler5(dependencyContainer);
8191
9597
  this.appHandler = appHandler(dependencyContainer);
9598
+ this.wsConnect = connect(dependencyContainer);
9599
+ this.wsDisconnect = disconnect(dependencyContainer);
9600
+ this.wsDefault = $default(dependencyContainer);
9601
+ this.wsBroadcast = broadcast(dependencyContainer);
8192
9602
  }
8193
9603
  };
8194
9604
  var index_default = CoreFactory;
@@ -8204,6 +9614,8 @@ export {
8204
9614
  StandardError,
8205
9615
  StandardErrorCode,
8206
9616
  TagRepository,
9617
+ TransactionService,
9618
+ WebSocketRepository,
8207
9619
  appHandler,
8208
9620
  handler as createEntityProcessor,
8209
9621
  index_default as default,
@@ -8211,6 +9623,11 @@ export {
8211
9623
  handler3 as prejoinProcessor,
8212
9624
  handler4 as replicationProcessor,
8213
9625
  setupCommonRoutes,
8214
- handler5 as tagProcessor
9626
+ handler5 as tagProcessor,
9627
+ transactional,
9628
+ broadcast as wsBroadcast,
9629
+ connect as wsConnect,
9630
+ $default as wsDefault,
9631
+ disconnect as wsDisconnect
8215
9632
  };
8216
9633
  //# sourceMappingURL=index.js.map