@abloatai/transaction 0.55.0 → 0.57.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.
Files changed (213) hide show
  1. package/CONVENTIONS.md +34 -0
  2. package/dist/auth/hostedEndpoints.d.ts +21 -5
  3. package/dist/auth/hostedEndpoints.d.ts.map +1 -1
  4. package/dist/auth/hostedEndpoints.js +21 -5
  5. package/dist/auth/hostedEndpoints.js.map +1 -1
  6. package/dist/auth/index.d.ts +1 -1
  7. package/dist/auth/index.d.ts.map +1 -1
  8. package/dist/auth/index.js +1 -1
  9. package/dist/auth/index.js.map +1 -1
  10. package/dist/coordination/index.d.ts +2 -2
  11. package/dist/coordination/index.d.ts.map +1 -1
  12. package/dist/coordination/index.js +1 -1
  13. package/dist/coordination/index.js.map +1 -1
  14. package/dist/coordination/schema.d.ts +0 -3
  15. package/dist/coordination/schema.d.ts.map +1 -1
  16. package/dist/coordination/schema.js +4 -5
  17. package/dist/coordination/schema.js.map +1 -1
  18. package/dist/errorCodes.d.ts +1 -0
  19. package/dist/errorCodes.d.ts.map +1 -1
  20. package/dist/errorCodes.js +1 -0
  21. package/dist/errorCodes.js.map +1 -1
  22. package/dist/errors.d.ts.map +1 -1
  23. package/dist/errors.js +4 -1
  24. package/dist/errors.js.map +1 -1
  25. package/dist/log/syncDeltaRow.d.ts +3 -3
  26. package/dist/readSetContext.d.ts.map +1 -1
  27. package/dist/readSetContext.js +8 -3
  28. package/dist/readSetContext.js.map +1 -1
  29. package/dist/resources/httpResources.d.ts +43 -22
  30. package/dist/resources/httpResources.d.ts.map +1 -1
  31. package/dist/resources/httpResources.js +81 -4
  32. package/dist/resources/httpResources.js.map +1 -1
  33. package/dist/resources/modelCreate.d.ts +21 -0
  34. package/dist/resources/modelCreate.d.ts.map +1 -0
  35. package/dist/resources/modelCreate.js +49 -0
  36. package/dist/resources/modelCreate.js.map +1 -0
  37. package/dist/resources/modelOperations.d.ts +24 -2
  38. package/dist/resources/modelOperations.d.ts.map +1 -1
  39. package/dist/resources/modelOperations.js.map +1 -1
  40. package/dist/resources/writeOptionsSchema.d.ts +14 -0
  41. package/dist/resources/writeOptionsSchema.d.ts.map +1 -1
  42. package/dist/resources/writeOptionsSchema.js +23 -2
  43. package/dist/resources/writeOptionsSchema.js.map +1 -1
  44. package/dist/schema/audit.d.ts +15 -0
  45. package/dist/schema/audit.d.ts.map +1 -0
  46. package/dist/schema/audit.js +90 -0
  47. package/dist/schema/audit.js.map +1 -0
  48. package/dist/schema/ddl.d.ts.map +1 -1
  49. package/dist/schema/ddl.js +68 -1
  50. package/dist/schema/ddl.js.map +1 -1
  51. package/dist/schema/index.d.ts +3 -1
  52. package/dist/schema/index.d.ts.map +1 -1
  53. package/dist/schema/index.js +3 -1
  54. package/dist/schema/index.js.map +1 -1
  55. package/dist/schema/model.d.ts +14 -0
  56. package/dist/schema/model.d.ts.map +1 -1
  57. package/dist/schema/model.js +2 -0
  58. package/dist/schema/model.js.map +1 -1
  59. package/dist/schema/openapi.d.ts.map +1 -1
  60. package/dist/schema/openapi.js +104 -2
  61. package/dist/schema/openapi.js.map +1 -1
  62. package/dist/schema/roles.d.ts +27 -0
  63. package/dist/schema/roles.d.ts.map +1 -1
  64. package/dist/schema/roles.js +40 -0
  65. package/dist/schema/roles.js.map +1 -1
  66. package/dist/schema/schema.d.ts +23 -4
  67. package/dist/schema/schema.d.ts.map +1 -1
  68. package/dist/schema/schema.js +30 -1
  69. package/dist/schema/schema.js.map +1 -1
  70. package/dist/schema/serialize.d.ts +4 -1
  71. package/dist/schema/serialize.d.ts.map +1 -1
  72. package/dist/schema/serialize.js +5 -1
  73. package/dist/schema/serialize.js.map +1 -1
  74. package/dist/schema/subject.d.ts +18 -0
  75. package/dist/schema/subject.d.ts.map +1 -0
  76. package/dist/schema/subject.js +27 -0
  77. package/dist/schema/subject.js.map +1 -0
  78. package/dist/server/adapter.d.ts +2 -0
  79. package/dist/server/adapter.d.ts.map +1 -1
  80. package/dist/server/readConfig.d.ts +3 -0
  81. package/dist/server/readConfig.d.ts.map +1 -1
  82. package/dist/server/readConfig.js +0 -21
  83. package/dist/server/readConfig.js.map +1 -1
  84. package/dist/source/adapters/drizzle.d.ts.map +1 -1
  85. package/dist/source/adapters/drizzle.js +37 -6
  86. package/dist/source/adapters/drizzle.js.map +1 -1
  87. package/dist/source/adapters/kysely.d.ts +2 -0
  88. package/dist/source/adapters/kysely.d.ts.map +1 -1
  89. package/dist/source/adapters/kysely.js +33 -5
  90. package/dist/source/adapters/kysely.js.map +1 -1
  91. package/dist/source/adapters/kyselyMutationCore.d.ts +4 -1
  92. package/dist/source/adapters/kyselyMutationCore.d.ts.map +1 -1
  93. package/dist/source/adapters/kyselyMutationCore.js +17 -8
  94. package/dist/source/adapters/kyselyMutationCore.js.map +1 -1
  95. package/dist/source/adapters/memory.d.ts.map +1 -1
  96. package/dist/source/adapters/memory.js +1 -0
  97. package/dist/source/adapters/memory.js.map +1 -1
  98. package/dist/source/adapters/prisma.d.ts.map +1 -1
  99. package/dist/source/adapters/prisma.js +59 -7
  100. package/dist/source/adapters/prisma.js.map +1 -1
  101. package/dist/source/contract.d.ts +14 -0
  102. package/dist/source/contract.d.ts.map +1 -1
  103. package/dist/source/contract.js +10 -0
  104. package/dist/source/contract.js.map +1 -1
  105. package/dist/source/factory.d.ts +7 -1
  106. package/dist/source/factory.d.ts.map +1 -1
  107. package/dist/source/factory.js +116 -5
  108. package/dist/source/factory.js.map +1 -1
  109. package/dist/source/index.d.ts +2 -1
  110. package/dist/source/index.d.ts.map +1 -1
  111. package/dist/source/index.js +1 -0
  112. package/dist/source/index.js.map +1 -1
  113. package/dist/source/migrations.d.ts.map +1 -1
  114. package/dist/source/migrations.js +21 -0
  115. package/dist/source/migrations.js.map +1 -1
  116. package/dist/source/subjectAuthorization.d.ts +16 -0
  117. package/dist/source/subjectAuthorization.d.ts.map +1 -0
  118. package/dist/source/subjectAuthorization.js +126 -0
  119. package/dist/source/subjectAuthorization.js.map +1 -0
  120. package/dist/source/types.d.ts +52 -2
  121. package/dist/source/types.d.ts.map +1 -1
  122. package/dist/source/types.js +1 -0
  123. package/dist/source/types.js.map +1 -1
  124. package/dist/syncLog/contract.d.ts +45 -3
  125. package/dist/syncLog/contract.d.ts.map +1 -1
  126. package/dist/syncLog/contract.js +46 -4
  127. package/dist/syncLog/contract.js.map +1 -1
  128. package/dist/testing/fixtures/httpResponses.d.ts +6 -0
  129. package/dist/testing/fixtures/httpResponses.d.ts.map +1 -1
  130. package/dist/testing/fixtures/httpResponses.js +1 -0
  131. package/dist/testing/fixtures/httpResponses.js.map +1 -1
  132. package/dist/transport/httpClient.d.ts +8 -1
  133. package/dist/transport/httpClient.d.ts.map +1 -1
  134. package/dist/transport/httpClient.js +51 -20
  135. package/dist/transport/httpClient.js.map +1 -1
  136. package/dist/transport/httpCommitRequest.d.ts +45 -0
  137. package/dist/transport/httpCommitRequest.d.ts.map +1 -0
  138. package/dist/transport/httpCommitRequest.js +67 -0
  139. package/dist/transport/httpCommitRequest.js.map +1 -0
  140. package/dist/transport/httpTransport.d.ts.map +1 -1
  141. package/dist/transport/httpTransport.js +69 -106
  142. package/dist/transport/httpTransport.js.map +1 -1
  143. package/dist/transport/httpTransportHelpers.d.ts +10 -0
  144. package/dist/transport/httpTransportHelpers.d.ts.map +1 -0
  145. package/dist/transport/httpTransportHelpers.js +46 -0
  146. package/dist/transport/httpTransportHelpers.js.map +1 -0
  147. package/dist/wire/apiLifecycle.d.ts +63 -0
  148. package/dist/wire/apiLifecycle.d.ts.map +1 -0
  149. package/dist/wire/apiLifecycle.js +89 -0
  150. package/dist/wire/apiLifecycle.js.map +1 -0
  151. package/dist/wire/auth.d.ts.map +1 -1
  152. package/dist/wire/auth.js +6 -3
  153. package/dist/wire/auth.js.map +1 -1
  154. package/dist/wire/commit.d.ts +74 -9
  155. package/dist/wire/commit.d.ts.map +1 -1
  156. package/dist/wire/commit.js +18 -5
  157. package/dist/wire/commit.js.map +1 -1
  158. package/dist/wire/index.d.ts +5 -2
  159. package/dist/wire/index.d.ts.map +1 -1
  160. package/dist/wire/index.js +12 -1
  161. package/dist/wire/index.js.map +1 -1
  162. package/dist/wire/modelMutations.js +3 -2
  163. package/dist/wire/modelMutations.js.map +1 -1
  164. package/dist/wire/rateLimit.d.ts +82 -0
  165. package/dist/wire/rateLimit.d.ts.map +1 -0
  166. package/dist/wire/rateLimit.js +142 -0
  167. package/dist/wire/rateLimit.js.map +1 -0
  168. package/package.json +1 -1
  169. package/src/auth/hostedEndpoints.ts +23 -5
  170. package/src/auth/index.ts +2 -0
  171. package/src/coordination/index.ts +0 -2
  172. package/src/coordination/schema.ts +4 -7
  173. package/src/errorCodes.ts +6 -0
  174. package/src/errors.ts +4 -1
  175. package/src/readSetContext.ts +8 -2
  176. package/src/resources/httpResources.ts +116 -25
  177. package/src/resources/modelCreate.ts +73 -0
  178. package/src/resources/modelOperations.ts +29 -2
  179. package/src/resources/writeOptionsSchema.ts +30 -2
  180. package/src/schema/audit.ts +121 -0
  181. package/src/schema/ddl.ts +71 -1
  182. package/src/schema/index.ts +16 -0
  183. package/src/schema/model.ts +17 -0
  184. package/src/schema/openapi.ts +123 -2
  185. package/src/schema/roles.ts +53 -0
  186. package/src/schema/schema.ts +68 -3
  187. package/src/schema/serialize.ts +8 -1
  188. package/src/schema/subject.ts +43 -0
  189. package/src/server/adapter.ts +2 -0
  190. package/src/server/readConfig.ts +4 -0
  191. package/src/source/adapters/drizzle.ts +50 -6
  192. package/src/source/adapters/kysely.ts +47 -4
  193. package/src/source/adapters/kyselyMutationCore.ts +21 -9
  194. package/src/source/adapters/memory.ts +1 -0
  195. package/src/source/adapters/prisma.ts +70 -7
  196. package/src/source/contract.ts +11 -0
  197. package/src/source/factory.ts +141 -5
  198. package/src/source/index.ts +6 -0
  199. package/src/source/migrations.ts +21 -0
  200. package/src/source/subjectAuthorization.ts +182 -0
  201. package/src/source/types.ts +56 -2
  202. package/src/syncLog/contract.ts +47 -5
  203. package/src/testing/fixtures/httpResponses.ts +7 -0
  204. package/src/transport/httpClient.ts +85 -23
  205. package/src/transport/httpCommitRequest.ts +104 -0
  206. package/src/transport/httpTransport.ts +92 -142
  207. package/src/transport/httpTransportHelpers.ts +63 -0
  208. package/src/wire/apiLifecycle.ts +94 -0
  209. package/src/wire/auth.ts +6 -3
  210. package/src/wire/commit.ts +20 -5
  211. package/src/wire/index.ts +31 -0
  212. package/src/wire/modelMutations.ts +2 -2
  213. package/src/wire/rateLimit.ts +155 -0
@@ -1 +1 @@
1
- {"version":3,"file":"modelMutations.js","sourceRoot":"","sources":["../../src/wire/modelMutations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,2BAA2B,EAC3B,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,2BAA2B,CAAC;AAEnC,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,gFAAgF;IAChF,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;IACjD;;;;;;;;;;;OAWG;IACH,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE;IACxB,gEAAgE;IAChE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE;IAC3B,2EAA2E;IAC3E,OAAO,EAAE,iBAAiB,CAAC,OAAO,EAAE;IACpC;;;;OAIG;IACH,MAAM,EAAE,sBAAsB,CAAC,OAAO,EAAE;IACxC,4EAA4E;IAC5E,KAAK,EAAE,wBAAwB,CAAC,OAAO,EAAE;IACzC,uEAAuE;IACvE,KAAK,EAAE,yBAAyB,CAAC,OAAO,EAAE;IAC1C;;;OAGG;IACH,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE;IAChC,wEAAwE;IACxE,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACtC,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,2BAA2B,CAAC,KAAK,CAAC,IAAI,oBAAoB,EAAE;IAC/E,IAAI,EAAE,CAAC,OAAO,CAAC;IACf,OAAO,EAAE,uCAAuC,oBAAoB,mBAAmB;CACxF,CAAC,CAAC"}
1
+ {"version":3,"file":"modelMutations.js","sourceRoot":"","sources":["../../src/wire/modelMutations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,2BAA2B,EAC3B,yBAAyB,GAC1B,MAAM,2BAA2B,CAAC;AAEnC,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IACjD,gFAAgF;IAChF,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;IACjD;;;;;;;;;;;OAWG;IACH,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE;IACxB,gEAAgE;IAChE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE;IAC3B,2EAA2E;IAC3E,OAAO,EAAE,iBAAiB,CAAC,OAAO,EAAE;IACpC;;;;OAIG;IACH,MAAM,EAAE,iBAAiB,CAAC,OAAO,EAAE;IACnC,4EAA4E;IAC5E,KAAK,EAAE,wBAAwB,CAAC,OAAO,EAAE;IACzC,uEAAuE;IACvE,KAAK,EAAE,yBAAyB,CAAC,OAAO,EAAE;IAC1C;;;OAGG;IACH,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE;IAChC,wEAAwE;IACxE,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACtC,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,2BAA2B,CAAC,KAAK,CAAC,IAAI,oBAAoB,EAAE;IAC/E,IAAI,EAAE,CAAC,OAAO,CAAC;IACf,OAAO,EAAE,uCAAuC,oBAAoB,mBAAmB;CACxF,CAAC,CAAC"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * How a rate limit is stated on the wire, so a caller can pace itself instead
3
+ * of discovering the ceiling by hitting it.
4
+ *
5
+ * Two fields, from "RateLimit header fields for HTTP"
6
+ * (draft-ietf-httpapi-ratelimit-headers), and they answer different questions:
7
+ *
8
+ * - `RateLimit-Policy` is the STANDING allowance — the quota and the window
9
+ * the server allocates. It does not move between responses, so it can be
10
+ * stated on any response, including one that carried no credential. This is
11
+ * the field an agent reads once and paces against.
12
+ * - `RateLimit` is the LIVE position within that allowance — what is left and
13
+ * when it refills. It is per caller, so it appears only once a request has
14
+ * been attributed to a credential.
15
+ *
16
+ * Both are Structured Field Lists whose members are Strings naming the policy,
17
+ * carrying `q`/`w` (quota, window seconds) and `r`/`t` (remaining, seconds to
18
+ * reset) as parameters. Serializing them by hand is the reason this module
19
+ * exists: the field names and the parameter spellings were previously written
20
+ * out at each producer, so a change had to be made in every one of them and
21
+ * nothing failed when it was not.
22
+ *
23
+ * `Retry-After` is not defined here — it is plain HTTP (RFC 9110 § 10.2.3) and
24
+ * the value is a count of seconds. Its NAME lives here so a producer emitting
25
+ * the 429 triple names all three fields from one place.
26
+ */
27
+ import { z } from 'zod';
28
+ /** The standing-allowance field. Safe on any response; independent of caller. */
29
+ export declare const RATE_LIMIT_POLICY_HEADER = "RateLimit-Policy";
30
+ /** The live-position field. Per caller, so only on an attributed request. */
31
+ export declare const RATE_LIMIT_HEADER = "RateLimit";
32
+ /** Seconds to wait before retrying, on a 429 or a 503 (RFC 9110 § 10.2.3). */
33
+ export declare const RETRY_AFTER_HEADER = "Retry-After";
34
+ /**
35
+ * One allocation of server capacity: how much, over how long.
36
+ *
37
+ * `quotaUnit` is omitted for the default unit, requests. A limiter that meters
38
+ * something else — bytes, operations — names it, and a client that does not
39
+ * recognize the unit knows to leave the number alone rather than read it as a
40
+ * request count.
41
+ */
42
+ export declare const quotaPolicySchema: z.ZodObject<{
43
+ name: z.ZodString;
44
+ quota: z.ZodNumber;
45
+ windowSeconds: z.ZodOptional<z.ZodNumber>;
46
+ quotaUnit: z.ZodOptional<z.ZodString>;
47
+ }, z.core.$strip>;
48
+ export type QuotaPolicy = Readonly<z.infer<typeof quotaPolicySchema>>;
49
+ /** Where this caller currently stands against one {@link QuotaPolicy}. */
50
+ export declare const serviceLimitSchema: z.ZodObject<{
51
+ policy: z.ZodString;
52
+ remaining: z.ZodNumber;
53
+ resetSeconds: z.ZodOptional<z.ZodNumber>;
54
+ }, z.core.$strip>;
55
+ export type ServiceLimit = Readonly<z.infer<typeof serviceLimitSchema>>;
56
+ /** Serialize {@link QuotaPolicy} values into a `RateLimit-Policy` field value. */
57
+ export declare function rateLimitPolicyField(policies: readonly QuotaPolicy[]): string;
58
+ /** Serialize {@link ServiceLimit} values into a `RateLimit` field value. */
59
+ export declare function rateLimitField(limits: readonly ServiceLimit[]): string;
60
+ /** What a producer knows about the limit at the moment it writes the response. */
61
+ export declare const rateLimitSignalSchema: z.ZodObject<{
62
+ policies: z.ZodReadonly<z.ZodArray<z.ZodObject<{
63
+ name: z.ZodString;
64
+ quota: z.ZodNumber;
65
+ windowSeconds: z.ZodOptional<z.ZodNumber>;
66
+ quotaUnit: z.ZodOptional<z.ZodString>;
67
+ }, z.core.$strip>>>;
68
+ limits: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodObject<{
69
+ policy: z.ZodString;
70
+ remaining: z.ZodNumber;
71
+ resetSeconds: z.ZodOptional<z.ZodNumber>;
72
+ }, z.core.$strip>>>>;
73
+ retryAfterSeconds: z.ZodOptional<z.ZodNumber>;
74
+ }, z.core.$strip>;
75
+ export type RateLimitSignal = Readonly<z.infer<typeof rateLimitSignalSchema>>;
76
+ /**
77
+ * The header map for one response. This is the single call every producer
78
+ * makes, so the three fields cannot be emitted in one place and forgotten in
79
+ * the next.
80
+ */
81
+ export declare function rateLimitHeaders(signal: RateLimitSignal): Record<string, string>;
82
+ //# sourceMappingURL=rateLimit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rateLimit.d.ts","sourceRoot":"","sources":["../../src/wire/rateLimit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,iFAAiF;AACjF,eAAO,MAAM,wBAAwB,qBAAqB,CAAC;AAE3D,6EAA6E;AAC7E,eAAO,MAAM,iBAAiB,cAAc,CAAC;AAE7C,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,gBAAgB,CAAC;AAWhD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB;;;;;iBAS5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC,CAAC;AAEtE,0EAA0E;AAC1E,eAAO,MAAM,kBAAkB;;;;iBAO7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC;AAoBxE,kFAAkF;AAClF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,GAAG,MAAM,CAa7E;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CAYtE;AAED,kFAAkF;AAClF,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;iBAOhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC,CAAC;AAE9E;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWhF"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * How a rate limit is stated on the wire, so a caller can pace itself instead
3
+ * of discovering the ceiling by hitting it.
4
+ *
5
+ * Two fields, from "RateLimit header fields for HTTP"
6
+ * (draft-ietf-httpapi-ratelimit-headers), and they answer different questions:
7
+ *
8
+ * - `RateLimit-Policy` is the STANDING allowance — the quota and the window
9
+ * the server allocates. It does not move between responses, so it can be
10
+ * stated on any response, including one that carried no credential. This is
11
+ * the field an agent reads once and paces against.
12
+ * - `RateLimit` is the LIVE position within that allowance — what is left and
13
+ * when it refills. It is per caller, so it appears only once a request has
14
+ * been attributed to a credential.
15
+ *
16
+ * Both are Structured Field Lists whose members are Strings naming the policy,
17
+ * carrying `q`/`w` (quota, window seconds) and `r`/`t` (remaining, seconds to
18
+ * reset) as parameters. Serializing them by hand is the reason this module
19
+ * exists: the field names and the parameter spellings were previously written
20
+ * out at each producer, so a change had to be made in every one of them and
21
+ * nothing failed when it was not.
22
+ *
23
+ * `Retry-After` is not defined here — it is plain HTTP (RFC 9110 § 10.2.3) and
24
+ * the value is a count of seconds. Its NAME lives here so a producer emitting
25
+ * the 429 triple names all three fields from one place.
26
+ */
27
+ import { z } from 'zod';
28
+ /** The standing-allowance field. Safe on any response; independent of caller. */
29
+ export const RATE_LIMIT_POLICY_HEADER = 'RateLimit-Policy';
30
+ /** The live-position field. Per caller, so only on an attributed request. */
31
+ export const RATE_LIMIT_HEADER = 'RateLimit';
32
+ /** Seconds to wait before retrying, on a 429 or a 503 (RFC 9110 § 10.2.3). */
33
+ export const RETRY_AFTER_HEADER = 'Retry-After';
34
+ /**
35
+ * A policy name as it appears inside the fields. Constrained to characters that
36
+ * need no Structured-Fields escaping, which is what lets the serializers below
37
+ * be a template rather than a String encoder — and what makes an unquotable
38
+ * name a thrown error at the definition site rather than a malformed header on
39
+ * the wire.
40
+ */
41
+ const POLICY_NAME = /^[A-Za-z0-9_-]{1,64}$/;
42
+ /**
43
+ * One allocation of server capacity: how much, over how long.
44
+ *
45
+ * `quotaUnit` is omitted for the default unit, requests. A limiter that meters
46
+ * something else — bytes, operations — names it, and a client that does not
47
+ * recognize the unit knows to leave the number alone rather than read it as a
48
+ * request count.
49
+ */
50
+ export const quotaPolicySchema = z.object({
51
+ /** Identifier the matching {@link ServiceLimit} refers back to. */
52
+ name: z.string(),
53
+ /** `q` — the allocation, in `quotaUnit`. */
54
+ quota: z.number(),
55
+ /** `w` — the window the allocation applies over, in whole seconds. */
56
+ windowSeconds: z.number().optional(),
57
+ /** `qu` — the unit `quota` counts. Omit for the default, requests. */
58
+ quotaUnit: z.string().optional(),
59
+ });
60
+ /** Where this caller currently stands against one {@link QuotaPolicy}. */
61
+ export const serviceLimitSchema = z.object({
62
+ /** The {@link QuotaPolicy} name this position is measured against. */
63
+ policy: z.string(),
64
+ /** `r` — units left in the current window. */
65
+ remaining: z.number(),
66
+ /** `t` — seconds until the allocation refills. */
67
+ resetSeconds: z.number().optional(),
68
+ });
69
+ function assertName(name, field) {
70
+ if (!POLICY_NAME.test(name)) {
71
+ throw new Error(`${field} policy name ${JSON.stringify(name)} must match ${String(POLICY_NAME)}`);
72
+ }
73
+ }
74
+ /**
75
+ * A non-negative whole number, which is what both fields' numeric parameters
76
+ * are. A fractional or negative value is a producer bug; clamping it here keeps
77
+ * one malformed reading from making the whole field unparseable for the client.
78
+ */
79
+ function integer(value) {
80
+ if (!Number.isFinite(value))
81
+ return 0;
82
+ return Math.max(0, Math.floor(value));
83
+ }
84
+ /** Serialize {@link QuotaPolicy} values into a `RateLimit-Policy` field value. */
85
+ export function rateLimitPolicyField(policies) {
86
+ if (policies.length === 0) {
87
+ throw new Error(`${RATE_LIMIT_POLICY_HEADER} must name at least one policy`);
88
+ }
89
+ return policies
90
+ .map((policy) => {
91
+ assertName(policy.name, RATE_LIMIT_POLICY_HEADER);
92
+ let item = `"${policy.name}";q=${integer(policy.quota)}`;
93
+ if (policy.quotaUnit !== undefined)
94
+ item += `;qu="${policy.quotaUnit}"`;
95
+ if (policy.windowSeconds !== undefined)
96
+ item += `;w=${integer(policy.windowSeconds)}`;
97
+ return item;
98
+ })
99
+ .join(', ');
100
+ }
101
+ /** Serialize {@link ServiceLimit} values into a `RateLimit` field value. */
102
+ export function rateLimitField(limits) {
103
+ if (limits.length === 0) {
104
+ throw new Error(`${RATE_LIMIT_HEADER} must name at least one policy`);
105
+ }
106
+ return limits
107
+ .map((limit) => {
108
+ assertName(limit.policy, RATE_LIMIT_HEADER);
109
+ let item = `"${limit.policy}";r=${integer(limit.remaining)}`;
110
+ if (limit.resetSeconds !== undefined)
111
+ item += `;t=${integer(limit.resetSeconds)}`;
112
+ return item;
113
+ })
114
+ .join(', ');
115
+ }
116
+ /** What a producer knows about the limit at the moment it writes the response. */
117
+ export const rateLimitSignalSchema = z.object({
118
+ /** The standing allowance. Always known; stated on every response. */
119
+ policies: z.array(quotaPolicySchema).readonly(),
120
+ /** This caller's position, once the request has been attributed to one. */
121
+ limits: z.array(serviceLimitSchema).readonly().optional(),
122
+ /** Present only on a rejection, and only when a wait is what resolves it. */
123
+ retryAfterSeconds: z.number().optional(),
124
+ });
125
+ /**
126
+ * The header map for one response. This is the single call every producer
127
+ * makes, so the three fields cannot be emitted in one place and forgotten in
128
+ * the next.
129
+ */
130
+ export function rateLimitHeaders(signal) {
131
+ const headers = {
132
+ [RATE_LIMIT_POLICY_HEADER]: rateLimitPolicyField(signal.policies),
133
+ };
134
+ if (signal.limits && signal.limits.length > 0) {
135
+ headers[RATE_LIMIT_HEADER] = rateLimitField(signal.limits);
136
+ }
137
+ if (signal.retryAfterSeconds !== undefined) {
138
+ headers[RETRY_AFTER_HEADER] = String(Math.max(1, integer(signal.retryAfterSeconds)));
139
+ }
140
+ return headers;
141
+ }
142
+ //# sourceMappingURL=rateLimit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/wire/rateLimit.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/transaction",
3
- "version": "0.55.0",
3
+ "version": "0.57.0",
4
4
  "description": "The headless Ablo transaction client and canonical contracts for reads, commits, confirmation, claims, and durable observation.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,10 +1,13 @@
1
1
  /**
2
- * The hosted Ablo Cloud endpoint constants, with no dependencies of their own.
2
+ * The hosted Ablo hosts, with no dependencies of their own.
3
3
  *
4
- * This is the single place the hosted API host is declared. URL resolution, the
5
- * CLI's default URL, the data-source connector's base, the generated OpenAPI
6
- * server entry, and the network probe's default all import from here, so
7
- * changing the API domain is a one-line edit.
4
+ * This is the single place each published Ablo domain is declared: the API the
5
+ * SDK calls, the documentation site every error link and descriptor points at,
6
+ * and the marketing site that carries signup and the legal pages. URL
7
+ * resolution, the CLI's default URL, the data-source connector's base, the
8
+ * generated OpenAPI server entry, the error registry's `doc_url`, and the
9
+ * published discovery descriptors all import from here, so changing a domain is
10
+ * a one-line edit.
8
11
  *
9
12
  * These constants are kept dependency-free deliberately: several low-level
10
13
  * modules consume them, and routing those modules through the auth layer would
@@ -22,3 +25,18 @@ export const ABLO_HOSTED_HTTP_BASE_URL = `https://${ABLO_HOSTED_API_DOMAIN}`;
22
25
  * {@link ABLO_HOSTED_HTTP_BASE_URL}; kept as a distinct name because it is
23
26
  * the documented client-options default. */
24
27
  export const ABLO_DEFAULT_BASE_URL = ABLO_HOSTED_HTTP_BASE_URL;
28
+
29
+ /**
30
+ * The documentation site. Canonical target of every error `doc_url`, of the
31
+ * published OpenAPI document, and of the discovery descriptors under
32
+ * `/.well-known`, which is why it is declared beside the API host rather than
33
+ * retyped at each of those call sites.
34
+ */
35
+ export const ABLO_DOCS_BASE_URL = 'https://docs.abloatai.com';
36
+
37
+ /**
38
+ * The marketing site: signup, the CLI device-flow approval page, and the legal
39
+ * pages a machine-readable onboarding descriptor has to cite. The apex
40
+ * redirects here, so this is the form to publish.
41
+ */
42
+ export const ABLO_SITE_BASE_URL = 'https://www.abloatai.com';
package/src/auth/index.ts CHANGED
@@ -26,8 +26,10 @@ import type {
26
26
  } from './capability.js';
27
27
  export {
28
28
  ABLO_DEFAULT_BASE_URL,
29
+ ABLO_DOCS_BASE_URL,
29
30
  ABLO_HOSTED_API_DOMAIN,
30
31
  ABLO_HOSTED_HTTP_BASE_URL,
32
+ ABLO_SITE_BASE_URL,
31
33
  } from './hostedEndpoints.js';
32
34
  export { normalizeAbloBaseUrl } from './baseUrl.js';
33
35
 
@@ -23,7 +23,6 @@ export {
23
23
  // Layer 3 — optimistic stale-context
24
24
  onStaleModeSchema,
25
25
  MAX_READ_SET_ENTRIES,
26
- readSetWatermarkSchema,
27
26
  readSetRowTargetSchema,
28
27
  readSetGroupTargetSchema,
29
28
  commitReadSetTargetSchema,
@@ -117,7 +116,6 @@ export type {
117
116
  TargetRef,
118
117
  // Layer 3 — optimistic stale-context
119
118
  OnStaleMode,
120
- ReadSetWatermark,
121
119
  ReadSetRowTarget,
122
120
  ReadSetGroupTarget,
123
121
  CommitReadSetTarget,
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { logPositionSchema, type LogPosition } from '../syncLog/contract.js';
2
3
  import {
3
4
  syncGroupInputSchema,
4
5
  syncGroupRefSchema,
@@ -267,10 +268,6 @@ const streamTargetSchema = targetRefSchema
267
268
  export const onStaleModeSchema = z.enum(['reject', 'overwrite', 'notify']);
268
269
  export type OnStaleMode = z.infer<typeof onStaleModeSchema>;
269
270
 
270
- /** The exact authoritative position retained for one ReadSet entry. */
271
- export const readSetWatermarkSchema = z.number().int().nonnegative();
272
- export type ReadSetWatermark = z.infer<typeof readSetWatermarkSchema>;
273
-
274
271
  /** Maximum decision-input entries one logical commit may ask the server to scan. */
275
272
  export const MAX_READ_SET_ENTRIES = 500;
276
273
 
@@ -285,7 +282,7 @@ export type TrackOnStale = z.infer<typeof trackOnStaleSchema>;
285
282
  * claim — see the claim layer below.
286
283
  */
287
284
  export const writeGuardSchema = z.object({
288
- readAt: readSetWatermarkSchema.nullish(),
285
+ readAt: logPositionSchema.nullish(),
289
286
  onStale: onStaleModeSchema.nullish(),
290
287
  bypass: z.boolean().optional(),
291
288
  });
@@ -489,7 +486,7 @@ export type PersistedReadSetTarget = z.infer<typeof persistedReadSetTargetSchema
489
486
  /** One exact input to the decision made by this commit. */
490
487
  export const commitReadSetEntrySchema = z.object({
491
488
  target: commitReadSetTargetSchema,
492
- watermark: readSetWatermarkSchema,
489
+ watermark: logPositionSchema,
493
490
  lifetime: z.literal('commit'),
494
491
  onStale: onStaleModeSchema,
495
492
  });
@@ -498,7 +495,7 @@ export type CommitReadSetEntry = z.infer<typeof commitReadSetEntrySchema>;
498
495
  /** One exact input retained after the declaring commit settles. */
499
496
  export const persistedReadSetEntrySchema = z.object({
500
497
  target: persistedReadSetTargetSchema,
501
- watermark: readSetWatermarkSchema,
498
+ watermark: logPositionSchema,
502
499
  lifetime: z.literal('persisted'),
503
500
  onStale: trackOnStaleSchema,
504
501
  });
package/src/errorCodes.ts CHANGED
@@ -846,6 +846,12 @@ export const ERROR_CODES = {
846
846
  false,
847
847
  'The database rejected the write on a foreign-key constraint: a referenced row does not exist, or the row being deleted is still referenced by others. The error details name the constraint.'
848
848
  ),
849
+ entity_already_exists: wire(
850
+ 'conflict',
851
+ 409,
852
+ false,
853
+ 'A row already exists with this id. CREATE is strict; use UPDATE for an existing row.'
854
+ ),
849
855
  unique_violation: wire(
850
856
  'conflict',
851
857
  409,
package/src/errors.ts CHANGED
@@ -17,6 +17,9 @@
17
17
 
18
18
  import { z } from 'zod';
19
19
  import type { ErrorCode } from './errorCodes.js';
20
+ // Dependency-free by design, so the error registry can name the docs host
21
+ // without pulling the auth layer into this module's import graph.
22
+ import { ABLO_DOCS_BASE_URL } from './auth/hostedEndpoints.js';
20
23
  import { errorCodeSpec, classifyRecovery } from './errorCodes.js';
21
24
  import {
22
25
  wireClaimSummarySchema,
@@ -156,7 +159,7 @@ export class AbloError extends Error {
156
159
  * a `doc_url` automatically.
157
160
  */
158
161
  export function docUrlForCode(code: ErrorCode): string {
159
- return `https://docs.abloatai.com/errors#${code}`;
162
+ return `${ABLO_DOCS_BASE_URL}/errors#${code}`;
160
163
  }
161
164
 
162
165
  /** 401 — invalid/missing/expired credentials. */
@@ -97,9 +97,15 @@ export function publishCommitRecord(
97
97
  registry.commitRecords.set(record.id, record);
98
98
  try {
99
99
  const observed = registry.onCommitRecord?.(record);
100
- if (observed) void Promise.resolve(observed).catch(() => undefined);
101
- } catch {
100
+ if (observed) {
101
+ void Promise.resolve(observed).catch((error) => {
102
+ // The observer is outside the transaction outcome by contract.
103
+ void error;
104
+ });
105
+ }
106
+ } catch (error) {
102
107
  // Observability must never change a commit's outcome.
108
+ void error;
103
109
  }
104
110
  }
105
111
 
@@ -19,9 +19,12 @@ import type {
19
19
  CommitRecordList,
20
20
  CommitRecordListOptions,
21
21
  CommitRecordWhere,
22
+ CommitOperationBody,
23
+ ModelOperationAction,
22
24
  } from '../wire/commit.js';
23
25
  import type { LogListResponse, LogQuery } from '../wire/feedEvent.js';
24
26
  import type { ModelListEvidence } from '../wire/modelResponses.js';
27
+ import { AbloValidationError } from '../errors.js';
25
28
  // Re-exported, not redeclared. `wire/commit.ts` owns the commit-status vocabulary
26
29
  // and derives the waitable subset from it; this module serves that name to SDK
27
30
  // consumers. Restating the subset here as its own union produced a type that
@@ -63,6 +66,7 @@ import type {
63
66
  AwaitedClaimMethod,
64
67
  ModelTrackParams,
65
68
  ModelTrackResult,
69
+ ModelCreateManyParams,
66
70
  ServerReadOptions,
67
71
  } from './modelOperations.js';
68
72
 
@@ -77,12 +81,7 @@ import type {
77
81
  * `claim({ id })` — a durable claim handle for coordinated writes
78
82
  */
79
83
 
80
- export type ModelOperationAction =
81
- | 'create'
82
- | 'update'
83
- | 'delete'
84
- | 'archive'
85
- | 'unarchive';
84
+ export type { ModelOperationAction };
86
85
 
87
86
 
88
87
  /** @internal Transport envelope; the public typed client returns the row. */
@@ -130,20 +129,119 @@ export interface HttpTransportList<T = Record<string, unknown>> {
130
129
  * and a complete one the same value, so the caller with 500 matching rows got
131
130
  * 20 and no way to find out.
132
131
  */
133
- export type ModelList<T> = T[] & Pick<HttpTransportList<T>, 'hasMore' | 'nextCursor'>;
132
+ export type ModelList<T> = T[] & Pick<HttpTransportList<T>, 'hasMore' | 'nextCursor'> &
133
+ AsyncIterable<T>;
134
134
 
135
135
  /**
136
- * Attach the page state to the rows. Non-enumerable so the result stays
137
- * indistinguishable from a plain array everywhere the properties aren't read.
136
+ * How far a `for await` over a list will walk before it gives up.
137
+ *
138
+ * A cursor that stops advancing would otherwise spin forever. The bound is
139
+ * high enough that no real collection reaches it and low enough that a broken
140
+ * server is a failed read rather than a hung process.
141
+ */
142
+ const AUTO_PAGE_LIMIT = 10_000;
143
+ const LIST_ALL_PAGE_LIMIT = 100;
144
+
145
+ const nextPageFor = new WeakMap<object, (cursor: string) => Promise<ModelList<unknown>>>();
146
+
147
+ export interface ModelListWalkOptions {
148
+ readonly maxPages?: number;
149
+ readonly signal?: AbortSignal;
150
+ }
151
+
152
+ async function* walkModelList<T>(
153
+ first: ModelList<T>,
154
+ options: ModelListWalkOptions = {},
155
+ ): AsyncGenerator<T> {
156
+ const maxPages = options.maxPages ?? AUTO_PAGE_LIMIT;
157
+ if (!Number.isInteger(maxPages) || maxPages < 1) {
158
+ throw new AbloValidationError('maxPages must be a positive integer.', {
159
+ code: 'invalid_options',
160
+ param: 'maxPages',
161
+ });
162
+ }
163
+
164
+ let current = first;
165
+ for (let visited = 0; visited < maxPages; visited += 1) {
166
+ options.signal?.throwIfAborted();
167
+ for (let i = 0; i < current.length; i += 1) {
168
+ options.signal?.throwIfAborted();
169
+ yield current[i] as T;
170
+ }
171
+ const cursor = current.nextCursor;
172
+ const fetchNext = nextPageFor.get(current) as
173
+ | ((nextCursor: string) => Promise<ModelList<T>>)
174
+ | undefined;
175
+ if (!current.hasMore || cursor === null || fetchNext === undefined) return;
176
+ const next = await fetchNext(cursor);
177
+ if (next.nextCursor === cursor) {
178
+ throw new AbloValidationError(
179
+ `Walking this list received the same continuation cursor twice (${JSON.stringify(cursor)}). ` +
180
+ 'The collection may be incomplete, so traversal stopped with an error.',
181
+ { code: 'malformed_response', param: 'nextCursor' },
182
+ );
183
+ }
184
+ current = next;
185
+ }
186
+ throw new AbloValidationError(
187
+ `Walking this list passed ${maxPages} pages without reaching the end. ` +
188
+ `Narrow the read with \`where\` or raise \`maxPages\` deliberately.`,
189
+ { code: 'invalid_options', param: 'maxPages' },
190
+ );
191
+ }
192
+
193
+ /** Collect a complete list through the same guarded cursor loop as async iteration. */
194
+ export async function collectModelList<T>(
195
+ first: ModelList<T>,
196
+ options: ModelListWalkOptions = {},
197
+ ): Promise<T[]> {
198
+ const rows: T[] = [];
199
+ for await (const row of walkModelList(first, {
200
+ maxPages: options.maxPages ?? LIST_ALL_PAGE_LIMIT,
201
+ signal: options.signal,
202
+ })) rows.push(row);
203
+ return rows;
204
+ }
205
+
206
+ /**
207
+ * Attach the page state to the rows, and make the list walk its own pages.
208
+ *
209
+ * `hasMore` and `nextCursor` are non-enumerable, so the result stays
210
+ * indistinguishable from a plain array everywhere they are not read: it maps,
211
+ * filters, spreads, and `JSON.stringify`s exactly as the rows always did.
212
+ *
213
+ * The async iterator is the answer to the question that shape raises. A list
214
+ * read is a page — the server applies a default size and caps the largest —
215
+ * and a page of 20 looks precisely like a complete answer of 20, so every
216
+ * caller either checked `hasMore` or, far more often, reasoned about a
217
+ * truncated set without knowing it. Hand-rolled page walkers were the common
218
+ * result, and each one re-derived the same cursor loop and the same
219
+ * non-advancing-cursor guard.
220
+ *
221
+ * So iterate the value to get the page, and `for await` it to get the
222
+ * collection:
223
+ *
224
+ * ```ts
225
+ * const page = await ablo.issue.list({ where: { teamId } });
226
+ * for (const issue of page) … // the 20 rows that came back
227
+ * for await (const issue of page) … // every issue, paged as it goes
228
+ * ```
138
229
  */
139
230
  export function modelList<T>(
140
231
  rows: readonly T[],
141
232
  page: Pick<HttpTransportList<unknown>, 'hasMore' | 'nextCursor'>,
233
+ /** Reads the page after `cursor`. Omitted where no transport can follow. */
234
+ fetchNext?: (cursor: string) => Promise<ModelList<T>>,
142
235
  ): ModelList<T> {
143
- return Object.defineProperties([...rows], {
236
+ const list = Object.defineProperties([...rows], {
144
237
  hasMore: { value: page.hasMore, enumerable: false },
145
238
  nextCursor: { value: page.nextCursor, enumerable: false },
239
+ [Symbol.asyncIterator]: { value: () => walkModelList(list), enumerable: false },
146
240
  }) as ModelList<T>;
241
+ if (fetchNext) {
242
+ nextPageFor.set(list, fetchNext as (cursor: string) => Promise<ModelList<unknown>>);
243
+ }
244
+ return list;
147
245
  }
148
246
 
149
247
  export type IfClaimedPolicy = 'return' | 'fail';
@@ -206,21 +304,8 @@ export interface ClaimCreateOptions {
206
304
  readonly onStatus?: (event: ClaimAttemptEvent) => void;
207
305
  }
208
306
 
209
- export interface CommitOperationInput {
210
- readonly action: ModelOperationAction;
211
- /** The model name — matches `ablo.<model>` and the schema's `model()`. */
212
- readonly model: string;
213
- readonly id?: string | null;
214
- readonly data?: Record<string, unknown> | null;
215
- readonly where?: Record<string, unknown> | null;
216
- readonly transactionId?: string | null;
217
- /** Claim identity derived from a held claim; not an application id. */
218
- readonly claimId?: string | null;
219
- readonly readAt?: number | null;
220
- readonly onStale?: OnStaleMode | null;
221
- /** Fencing token (Option B) from the batch's claim handle; server-validated. */
222
- readonly fenceToken?: number | null;
223
- }
307
+ /** Public commit operation inferred from the canonical request-body schema. */
308
+ export type CommitOperationInput = CommitOperationBody;
224
309
 
225
310
  export interface CommitCreateOptions {
226
311
  readonly idempotencyKey?: string | null;
@@ -402,6 +487,12 @@ export interface HttpTransportModel<
402
487
  * returned, not the input.
403
488
  */
404
489
  create(params: ModelMutationOptions & { readonly data: Record<string, unknown>; readonly id?: string | null }): Promise<T>;
490
+ /**
491
+ * Creates many rows as one atomic commit and returns them, in the caller's
492
+ * order. One rejected row declines the batch. The rows are the server's own,
493
+ * carried back on the commit rather than read again afterwards.
494
+ */
495
+ createMany(params: ModelCreateManyParams<Record<string, unknown>>): Promise<T[]>;
405
496
  update(params: ModelMutationOptions & { readonly id: string; readonly data: Record<string, unknown> }): Promise<CommitReceipt>;
406
497
  /**
407
498
  * Update under contention with a function of the latest state —