on-zero 0.7.2-1784530701923 → 0.7.2

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 (260) hide show
  1. package/dist/cjs/asyncTasks.cjs +51 -0
  2. package/dist/cjs/asyncTasks.native.js +84 -0
  3. package/dist/cjs/asyncTasks.native.js.map +1 -0
  4. package/dist/cjs/createZeroDirectServer.cjs +269 -0
  5. package/dist/cjs/createZeroDirectServer.native.js +287 -0
  6. package/dist/cjs/createZeroDirectServer.native.js.map +1 -0
  7. package/dist/cjs/createZeroSQLiteServer.cjs +50 -0
  8. package/dist/cjs/createZeroSQLiteServer.native.js +58 -0
  9. package/dist/cjs/createZeroSQLiteServer.native.js.map +1 -0
  10. package/dist/cjs/createZeroSQLiteServer.test.cjs +595 -0
  11. package/dist/cjs/createZeroSQLiteServer.test.native.js +647 -0
  12. package/dist/cjs/createZeroSQLiteServer.test.native.js.map +1 -0
  13. package/dist/cjs/createZeroServer.cjs +82 -0
  14. package/dist/cjs/createZeroServer.native.js +93 -0
  15. package/dist/cjs/createZeroServer.native.js.map +1 -0
  16. package/dist/cjs/createZeroServer.test.cjs +378 -0
  17. package/dist/cjs/createZeroServer.test.native.js +448 -0
  18. package/dist/cjs/createZeroServer.test.native.js.map +1 -0
  19. package/dist/cjs/createZeroServerCore.cjs +211 -0
  20. package/dist/cjs/createZeroServerCore.native.js +355 -0
  21. package/dist/cjs/createZeroServerCore.native.js.map +1 -0
  22. package/dist/cjs/helpers/createMutators.cjs +3 -4
  23. package/dist/cjs/helpers/createMutators.native.js +3 -6
  24. package/dist/cjs/helpers/createMutators.native.js.map +1 -1
  25. package/dist/cjs/httpPull/auth.test.cjs +197 -0
  26. package/dist/cjs/httpPull/auth.test.native.js +279 -0
  27. package/dist/cjs/httpPull/auth.test.native.js.map +1 -0
  28. package/dist/cjs/httpPull/churn.test.cjs +132 -0
  29. package/dist/cjs/httpPull/churn.test.native.js +155 -0
  30. package/dist/cjs/httpPull/churn.test.native.js.map +1 -0
  31. package/dist/cjs/httpPull/completeAck.test.cjs +69 -0
  32. package/dist/cjs/httpPull/completeAck.test.native.js +76 -0
  33. package/dist/cjs/httpPull/completeAck.test.native.js.map +1 -0
  34. package/dist/cjs/httpPull/fixtureServer.cjs +340 -0
  35. package/dist/cjs/httpPull/fixtureServer.native.js +534 -0
  36. package/dist/cjs/httpPull/fixtureServer.native.js.map +1 -0
  37. package/dist/cjs/httpPull/integration.test.cjs +53 -0
  38. package/dist/cjs/httpPull/integration.test.native.js +60 -0
  39. package/dist/cjs/httpPull/integration.test.native.js.map +1 -0
  40. package/dist/cjs/httpPull/rebase.test.cjs +360 -0
  41. package/dist/cjs/httpPull/rebase.test.native.js +420 -0
  42. package/dist/cjs/httpPull/rebase.test.native.js.map +1 -0
  43. package/dist/cjs/httpPull/relations.test.cjs +107 -0
  44. package/dist/cjs/httpPull/relations.test.native.js +119 -0
  45. package/dist/cjs/httpPull/relations.test.native.js.map +1 -0
  46. package/dist/cjs/httpPull/testHarness.cjs +100 -0
  47. package/dist/cjs/httpPull/testHarness.native.js +112 -0
  48. package/dist/cjs/httpPull/testHarness.native.js.map +1 -0
  49. package/dist/cjs/index.cjs +1 -0
  50. package/dist/cjs/index.native.js +1 -0
  51. package/dist/cjs/index.native.js.map +1 -1
  52. package/dist/cjs/run.cjs +1 -1
  53. package/dist/cjs/run.native.js +1 -1
  54. package/dist/cjs/server.cjs +22 -0
  55. package/dist/cjs/server.native.js +4 -0
  56. package/dist/cjs/server.native.js.map +1 -0
  57. package/dist/cjs/sqliteRuntime.cjs +318 -0
  58. package/dist/cjs/sqliteRuntime.native.js +438 -0
  59. package/dist/cjs/sqliteRuntime.native.js.map +1 -0
  60. package/dist/cjs/state.cjs +1 -1
  61. package/dist/cjs/state.native.js +1 -1
  62. package/dist/cjs/syncHost.cjs +179 -0
  63. package/dist/cjs/syncHost.native.js +388 -0
  64. package/dist/cjs/syncHost.native.js.map +1 -0
  65. package/dist/cjs/syncHost.test.cjs +240 -0
  66. package/dist/cjs/syncHost.test.native.js +290 -0
  67. package/dist/cjs/syncHost.test.native.js.map +1 -0
  68. package/dist/cjs/zeroRunner.cjs +1 -1
  69. package/dist/cjs/zeroRunner.native.js +1 -1
  70. package/dist/esm/asyncTasks.mjs +26 -0
  71. package/dist/esm/asyncTasks.mjs.map +1 -0
  72. package/dist/esm/asyncTasks.native.js +56 -0
  73. package/dist/esm/asyncTasks.native.js.map +1 -0
  74. package/dist/esm/createZeroDirectServer.mjs +241 -0
  75. package/dist/esm/createZeroDirectServer.mjs.map +1 -0
  76. package/dist/esm/createZeroDirectServer.native.js +256 -0
  77. package/dist/esm/createZeroDirectServer.native.js.map +1 -0
  78. package/dist/esm/createZeroSQLiteServer.mjs +24 -0
  79. package/dist/esm/createZeroSQLiteServer.mjs.map +1 -0
  80. package/dist/esm/createZeroSQLiteServer.native.js +29 -0
  81. package/dist/esm/createZeroSQLiteServer.native.js.map +1 -0
  82. package/dist/esm/createZeroSQLiteServer.test.mjs +596 -0
  83. package/dist/esm/createZeroSQLiteServer.test.mjs.map +1 -0
  84. package/dist/esm/createZeroSQLiteServer.test.native.js +645 -0
  85. package/dist/esm/createZeroSQLiteServer.test.native.js.map +1 -0
  86. package/dist/esm/createZeroServer.mjs +57 -0
  87. package/dist/esm/createZeroServer.mjs.map +1 -0
  88. package/dist/esm/createZeroServer.native.js +65 -0
  89. package/dist/esm/createZeroServer.native.js.map +1 -0
  90. package/dist/esm/createZeroServer.test.mjs +379 -0
  91. package/dist/esm/createZeroServer.test.mjs.map +1 -0
  92. package/dist/esm/createZeroServer.test.native.js +446 -0
  93. package/dist/esm/createZeroServer.test.native.js.map +1 -0
  94. package/dist/esm/createZeroServerCore.mjs +186 -0
  95. package/dist/esm/createZeroServerCore.mjs.map +1 -0
  96. package/dist/esm/createZeroServerCore.native.js +327 -0
  97. package/dist/esm/createZeroServerCore.native.js.map +1 -0
  98. package/dist/esm/helpers/createMutators.mjs +3 -4
  99. package/dist/esm/helpers/createMutators.mjs.map +1 -1
  100. package/dist/esm/helpers/createMutators.native.js +3 -6
  101. package/dist/esm/helpers/createMutators.native.js.map +1 -1
  102. package/dist/esm/httpPull/auth.test.mjs +198 -0
  103. package/dist/esm/httpPull/auth.test.mjs.map +1 -0
  104. package/dist/esm/httpPull/auth.test.native.js +277 -0
  105. package/dist/esm/httpPull/auth.test.native.js.map +1 -0
  106. package/dist/esm/httpPull/churn.test.mjs +133 -0
  107. package/dist/esm/httpPull/churn.test.mjs.map +1 -0
  108. package/dist/esm/httpPull/churn.test.native.js +153 -0
  109. package/dist/esm/httpPull/churn.test.native.js.map +1 -0
  110. package/dist/esm/httpPull/completeAck.test.mjs +70 -0
  111. package/dist/esm/httpPull/completeAck.test.mjs.map +1 -0
  112. package/dist/esm/httpPull/completeAck.test.native.js +74 -0
  113. package/dist/esm/httpPull/completeAck.test.native.js.map +1 -0
  114. package/dist/esm/httpPull/fixtureServer.mjs +315 -0
  115. package/dist/esm/httpPull/fixtureServer.mjs.map +1 -0
  116. package/dist/esm/httpPull/fixtureServer.native.js +506 -0
  117. package/dist/esm/httpPull/fixtureServer.native.js.map +1 -0
  118. package/dist/esm/httpPull/integration.test.mjs +54 -0
  119. package/dist/esm/httpPull/integration.test.mjs.map +1 -0
  120. package/dist/esm/httpPull/integration.test.native.js +58 -0
  121. package/dist/esm/httpPull/integration.test.native.js.map +1 -0
  122. package/dist/esm/httpPull/rebase.test.mjs +361 -0
  123. package/dist/esm/httpPull/rebase.test.mjs.map +1 -0
  124. package/dist/esm/httpPull/rebase.test.native.js +418 -0
  125. package/dist/esm/httpPull/rebase.test.native.js.map +1 -0
  126. package/dist/esm/httpPull/relations.test.mjs +108 -0
  127. package/dist/esm/httpPull/relations.test.mjs.map +1 -0
  128. package/dist/esm/httpPull/relations.test.native.js +117 -0
  129. package/dist/esm/httpPull/relations.test.native.js.map +1 -0
  130. package/dist/esm/httpPull/testHarness.mjs +72 -0
  131. package/dist/esm/httpPull/testHarness.mjs.map +1 -0
  132. package/dist/esm/httpPull/testHarness.native.js +81 -0
  133. package/dist/esm/httpPull/testHarness.native.js.map +1 -0
  134. package/dist/esm/index.js +1 -0
  135. package/dist/esm/index.js.map +1 -1
  136. package/dist/esm/index.mjs +1 -0
  137. package/dist/esm/index.mjs.map +1 -1
  138. package/dist/esm/index.native.js +1 -0
  139. package/dist/esm/index.native.js.map +1 -1
  140. package/dist/esm/run.mjs +1 -1
  141. package/dist/esm/run.mjs.map +1 -1
  142. package/dist/esm/run.native.js +1 -1
  143. package/dist/esm/server.mjs +4 -0
  144. package/dist/esm/server.mjs.map +1 -0
  145. package/dist/esm/server.native.js +2 -0
  146. package/dist/esm/server.native.js.map +1 -0
  147. package/dist/esm/sqliteRuntime.mjs +289 -0
  148. package/dist/esm/sqliteRuntime.mjs.map +1 -0
  149. package/dist/esm/sqliteRuntime.native.js +406 -0
  150. package/dist/esm/sqliteRuntime.native.js.map +1 -0
  151. package/dist/esm/state.mjs +1 -1
  152. package/dist/esm/state.native.js +1 -1
  153. package/dist/esm/syncHost.mjs +153 -0
  154. package/dist/esm/syncHost.mjs.map +1 -0
  155. package/dist/esm/syncHost.native.js +359 -0
  156. package/dist/esm/syncHost.native.js.map +1 -0
  157. package/dist/esm/syncHost.test.mjs +217 -0
  158. package/dist/esm/syncHost.test.mjs.map +1 -0
  159. package/dist/esm/syncHost.test.native.js +264 -0
  160. package/dist/esm/syncHost.test.native.js.map +1 -0
  161. package/dist/esm/zeroRunner.mjs +1 -1
  162. package/dist/esm/zeroRunner.mjs.map +1 -1
  163. package/dist/esm/zeroRunner.native.js +1 -1
  164. package/package.json +23 -8
  165. package/readme.md +65 -40
  166. package/src/asyncTasks.ts +36 -0
  167. package/src/createZeroClient.tsx +1 -1
  168. package/src/createZeroDirectServer.ts +411 -0
  169. package/src/createZeroSQLiteServer.test.ts +614 -0
  170. package/src/createZeroSQLiteServer.ts +56 -0
  171. package/src/createZeroServer.test.ts +421 -0
  172. package/src/createZeroServer.ts +105 -0
  173. package/src/createZeroServerCore.ts +296 -0
  174. package/src/helpers/createMutators.ts +2 -6
  175. package/src/httpPull/auth.test.ts +208 -0
  176. package/src/httpPull/churn.test.ts +147 -0
  177. package/src/httpPull/completeAck.test.ts +80 -0
  178. package/src/httpPull/fixtureServer.ts +391 -0
  179. package/src/httpPull/integration.test.ts +57 -0
  180. package/src/httpPull/rebase.test.ts +368 -0
  181. package/src/httpPull/relations.test.ts +135 -0
  182. package/src/httpPull/testHarness.ts +95 -0
  183. package/src/index.ts +1 -0
  184. package/src/run.ts +1 -1
  185. package/src/server.native.ts +3 -0
  186. package/src/server.ts +4 -0
  187. package/src/sqliteRuntime.ts +512 -0
  188. package/src/state.ts +1 -1
  189. package/src/syncHost.test.ts +223 -0
  190. package/src/syncHost.ts +294 -0
  191. package/src/types.ts +5 -0
  192. package/src/zeroRunner.ts +1 -1
  193. package/test-fixtures/server-return-types.ts +58 -21
  194. package/types/asyncTasks.d.ts +11 -0
  195. package/types/asyncTasks.d.ts.map +1 -0
  196. package/types/createZeroDirectServer.d.ts +80 -0
  197. package/types/createZeroDirectServer.d.ts.map +1 -0
  198. package/types/createZeroSQLiteServer.d.ts +12 -0
  199. package/types/createZeroSQLiteServer.d.ts.map +1 -0
  200. package/types/createZeroSQLiteServer.test.d.ts +2 -0
  201. package/types/createZeroSQLiteServer.test.d.ts.map +1 -0
  202. package/types/createZeroServer.d.ts +18 -0
  203. package/types/createZeroServer.d.ts.map +1 -0
  204. package/types/createZeroServer.test.d.ts +2 -0
  205. package/types/createZeroServer.test.d.ts.map +1 -0
  206. package/types/createZeroServerCore.d.ts +256 -0
  207. package/types/createZeroServerCore.d.ts.map +1 -0
  208. package/types/helpers/createMutators.d.ts +1 -2
  209. package/types/helpers/createMutators.d.ts.map +1 -1
  210. package/types/httpPull/auth.test.d.ts +2 -0
  211. package/types/httpPull/auth.test.d.ts.map +1 -0
  212. package/types/httpPull/churn.test.d.ts +2 -0
  213. package/types/httpPull/churn.test.d.ts.map +1 -0
  214. package/types/httpPull/completeAck.test.d.ts +2 -0
  215. package/types/httpPull/completeAck.test.d.ts.map +1 -0
  216. package/types/httpPull/fixtureSchema.d.ts +1 -1
  217. package/types/httpPull/fixtureServer.d.ts +14 -0
  218. package/types/httpPull/fixtureServer.d.ts.map +1 -0
  219. package/types/httpPull/integration.test.d.ts +2 -0
  220. package/types/httpPull/integration.test.d.ts.map +1 -0
  221. package/types/httpPull/rebase.test.d.ts +2 -0
  222. package/types/httpPull/rebase.test.d.ts.map +1 -0
  223. package/types/httpPull/relations.test.d.ts +2 -0
  224. package/types/httpPull/relations.test.d.ts.map +1 -0
  225. package/types/httpPull/testHarness.d.ts +32 -0
  226. package/types/httpPull/testHarness.d.ts.map +1 -0
  227. package/types/index.d.ts +1 -0
  228. package/types/index.d.ts.map +1 -1
  229. package/types/server.d.ts +5 -0
  230. package/types/server.d.ts.map +1 -0
  231. package/types/server.native.d.ts +1 -0
  232. package/types/server.native.d.ts.map +1 -0
  233. package/types/sqliteRuntime.d.ts +71 -0
  234. package/types/sqliteRuntime.d.ts.map +1 -0
  235. package/types/syncHost.d.ts +42 -0
  236. package/types/syncHost.d.ts.map +1 -0
  237. package/types/syncHost.test.d.ts +2 -0
  238. package/types/syncHost.test.d.ts.map +1 -0
  239. package/types/types.d.ts +4 -0
  240. package/types/types.d.ts.map +1 -1
  241. package/dist/cjs/orez.cjs +0 -227
  242. package/dist/cjs/orez.native.js +0 -314
  243. package/dist/cjs/orez.native.js.map +0 -1
  244. package/dist/cjs/orez.test.cjs +0 -211
  245. package/dist/cjs/orez.test.native.js +0 -226
  246. package/dist/cjs/orez.test.native.js.map +0 -1
  247. package/dist/esm/orez.mjs +0 -202
  248. package/dist/esm/orez.mjs.map +0 -1
  249. package/dist/esm/orez.native.js +0 -286
  250. package/dist/esm/orez.native.js.map +0 -1
  251. package/dist/esm/orez.test.mjs +0 -188
  252. package/dist/esm/orez.test.mjs.map +0 -1
  253. package/dist/esm/orez.test.native.js +0 -200
  254. package/dist/esm/orez.test.native.js.map +0 -1
  255. package/src/orez.test.ts +0 -195
  256. package/src/orez.ts +0 -331
  257. package/types/orez.d.ts +0 -53
  258. package/types/orez.d.ts.map +0 -1
  259. package/types/orez.test.d.ts +0 -2
  260. package/types/orez.test.d.ts.map +0 -1
@@ -0,0 +1,614 @@
1
+ import { DatabaseSync, type SQLInputValue } from 'node:sqlite'
2
+
3
+ import { boolean, createSchema, json, relationships, string, table } from '@rocicorp/zero'
4
+ import { asQueryInternals } from '@rocicorp/zero/bindings'
5
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest'
6
+
7
+ import { createZeroSQLiteServer } from './createZeroSQLiteServer'
8
+ import { zeroHttpFixtureSchema } from './httpPull/fixtureSchema'
9
+ import { mutations } from './mutations'
10
+ import { serverWhere } from './serverWhere'
11
+
12
+ import type {
13
+ SqlStatementMetadata,
14
+ SQLiteTransactionExecutor,
15
+ SQLiteTransactionProvider,
16
+ } from './createZeroSQLiteServer'
17
+
18
+ const note = table('note')
19
+ .from('note_records')
20
+ .columns({
21
+ id: string().from('note_id'),
22
+ done: boolean(),
23
+ data: json<{ label: string }>().optional(),
24
+ })
25
+ .primaryKey('id')
26
+
27
+ const sqliteTypesSchema = createSchema({
28
+ tables: [note],
29
+ relationships: [],
30
+ enableLegacyQueries: true,
31
+ })
32
+
33
+ const person = table('person').columns({ id: string() }).primaryKey('id')
34
+ const membership = table('membership')
35
+ .columns({ personId: string(), teamId: string() })
36
+ .primaryKey('personId', 'teamId')
37
+ const team = table('team')
38
+ .from('team_records')
39
+ .columns({
40
+ id: string().from('team_id'),
41
+ active: boolean().from('is_active'),
42
+ metadata: json<{ label: string }>().from('metadata_json'),
43
+ })
44
+ .primaryKey('id')
45
+ const twoHopSchema = createSchema({
46
+ tables: [person, membership, team],
47
+ relationships: [
48
+ relationships(person, ({ many }) => ({
49
+ teams: many(
50
+ {
51
+ sourceField: ['id'],
52
+ destSchema: membership,
53
+ destField: ['personId'],
54
+ },
55
+ {
56
+ sourceField: ['teamId'],
57
+ destSchema: team,
58
+ destField: ['id'],
59
+ },
60
+ ),
61
+ })),
62
+ ],
63
+ })
64
+
65
+ function pushRequest(id: number, name: string, args: unknown) {
66
+ return new Request('http://localhost/push?schema=zero_0&appID=zero', {
67
+ method: 'POST',
68
+ headers: { 'content-type': 'application/json' },
69
+ body: JSON.stringify({
70
+ clientGroupID: 'cg1',
71
+ mutations: [
72
+ {
73
+ type: 'custom',
74
+ id,
75
+ clientID: 'client-a',
76
+ name,
77
+ args: [args],
78
+ timestamp: 1,
79
+ },
80
+ ],
81
+ pushVersion: 1,
82
+ timestamp: 1,
83
+ requestID: 'req-' + id,
84
+ }),
85
+ })
86
+ }
87
+
88
+ function cleanupRequest() {
89
+ return new Request('http://localhost/push?schema=zero_0&appID=zero', {
90
+ method: 'POST',
91
+ headers: { 'content-type': 'application/json' },
92
+ body: JSON.stringify({
93
+ clientGroupID: 'cg1',
94
+ mutations: [
95
+ {
96
+ type: 'custom',
97
+ id: 2,
98
+ clientID: 'client-a',
99
+ name: '_zero_cleanupResults',
100
+ args: [
101
+ {
102
+ clientGroupID: 'cg1',
103
+ clientID: 'client-a',
104
+ upToMutationID: 1,
105
+ },
106
+ ],
107
+ timestamp: 1,
108
+ },
109
+ ],
110
+ pushVersion: 1,
111
+ timestamp: 1,
112
+ requestID: 'cleanup',
113
+ }),
114
+ })
115
+ }
116
+
117
+ describe('createZeroSQLiteServer', () => {
118
+ let db: DatabaseSync
119
+ let retryNextTransaction: boolean
120
+ let transactionProvider: SQLiteTransactionProvider
121
+ let statements: SqlStatementMetadata[]
122
+
123
+ beforeEach(() => {
124
+ db = new DatabaseSync(':memory:')
125
+ db.exec(
126
+ [
127
+ 'CREATE TABLE project (id TEXT PRIMARY KEY, ownerId TEXT NOT NULL, name TEXT NOT NULL);',
128
+ 'CREATE TABLE note_records (note_id TEXT PRIMARY KEY, done INTEGER NOT NULL, data TEXT);',
129
+ ].join('\n'),
130
+ )
131
+
132
+ retryNextTransaction = false
133
+ statements = []
134
+ const executor: SQLiteTransactionExecutor = {
135
+ async exec(sql, params = [], metadata) {
136
+ if (metadata) statements.push(metadata)
137
+ const result = db.prepare(sql).run(...(params as SQLInputValue[]))
138
+ return { changes: Number(result.changes) }
139
+ },
140
+ async query<Row extends Record<string, unknown>>(sql, params = []) {
141
+ return db.prepare(sql).all(...(params as SQLInputValue[])) as Row[]
142
+ },
143
+ async queryAst<Result>(ast, format) {
144
+ let rows: Record<string, unknown>[]
145
+ if (ast.table === 'project') {
146
+ rows = db.prepare('SELECT * FROM project ORDER BY id').all()
147
+ } else if (ast.table === 'note') {
148
+ rows = db
149
+ .prepare('SELECT note_id, done, data FROM note_records ORDER BY note_id')
150
+ .all()
151
+ .map((row) => ({
152
+ id: row.note_id,
153
+ done: row.done !== 0,
154
+ data: typeof row.data === 'string' ? JSON.parse(row.data) : row.data,
155
+ }))
156
+ } else if (ast.table === 'person') {
157
+ rows = [
158
+ {
159
+ id: 'p1',
160
+ teams: [
161
+ {
162
+ id: 't1',
163
+ active: true,
164
+ metadata: { label: 'core' },
165
+ },
166
+ ],
167
+ },
168
+ ]
169
+ } else {
170
+ throw new Error('unexpected table ' + ast.table)
171
+ }
172
+ return (format.singular ? rows[0] : rows) as Result
173
+ },
174
+ }
175
+ const run = async <R>(
176
+ callback: (tx: SQLiteTransactionExecutor) => R | Promise<R>,
177
+ ) => {
178
+ db.exec('BEGIN')
179
+ try {
180
+ const value = await callback(executor)
181
+ db.exec('COMMIT')
182
+ return value
183
+ } catch (error) {
184
+ db.exec('ROLLBACK')
185
+ throw error
186
+ }
187
+ }
188
+ transactionProvider = async <R>(
189
+ callback: (tx: SQLiteTransactionExecutor) => R | Promise<R>,
190
+ ) => {
191
+ if (retryNextTransaction) {
192
+ retryNextTransaction = false
193
+ db.exec('BEGIN')
194
+ await callback(executor)
195
+ db.exec('ROLLBACK')
196
+ }
197
+ return run(callback)
198
+ }
199
+ })
200
+
201
+ afterEach(() => {
202
+ db.close()
203
+ })
204
+
205
+ test('commits CRUD, the mutation ledger, and one retry-safe async task', async () => {
206
+ let effectRuns = 0
207
+ const server = createZeroSQLiteServer({
208
+ schema: zeroHttpFixtureSchema,
209
+ models: {
210
+ project: {
211
+ mutate: {
212
+ create: async ({ tx, server }, value: Record<string, unknown>) => {
213
+ await tx.mutate.project.insert(value as never)
214
+ server?.enqueueTask(
215
+ async () => {
216
+ effectRuns++
217
+ },
218
+ { barrier: true },
219
+ )
220
+ },
221
+ },
222
+ },
223
+ },
224
+ createServerActions: () => ({}),
225
+ transactionProvider,
226
+ })
227
+
228
+ retryNextTransaction = true
229
+ const result = await server.handleMutationRequest({
230
+ authData: null,
231
+ request: pushRequest(1, 'project.create', {
232
+ id: 'p1',
233
+ ownerId: 'u1',
234
+ name: 'first',
235
+ }),
236
+ skipAsyncTasks: true,
237
+ })
238
+
239
+ expect((result.response as any).mutations[0].result).toEqual({})
240
+ expect(db.prepare('SELECT * FROM project').all()).toEqual([
241
+ { id: 'p1', ownerId: 'u1', name: 'first' },
242
+ ])
243
+ expect(db.prepare('SELECT * FROM zero_0_clients').all()).toEqual([
244
+ { clientGroupID: 'cg1', clientID: 'client-a', lastMutationID: 1 },
245
+ ])
246
+ expect(result.asyncTasks).toHaveLength(1)
247
+ expect(result.asyncTasks[0]!.barrier).toBe(true)
248
+ await result.asyncTasks[0]!.task()
249
+ expect(effectRuns).toBe(1)
250
+ })
251
+
252
+ test('keeps later writes after inserting an existing primary key', async () => {
253
+ db.prepare('INSERT INTO project (id, ownerId, name) VALUES (?, ?, ?)').run(
254
+ 'p1',
255
+ 'u1',
256
+ 'original',
257
+ )
258
+ db.prepare('INSERT INTO project (id, ownerId, name) VALUES (?, ?, ?)').run(
259
+ 'p2',
260
+ 'u1',
261
+ 'before',
262
+ )
263
+ const server = createZeroSQLiteServer({
264
+ schema: zeroHttpFixtureSchema,
265
+ models: {
266
+ project: {
267
+ mutate: {
268
+ converge: async ({ tx }) => {
269
+ await tx.mutate.project.insert({
270
+ id: 'p1',
271
+ ownerId: 'u2',
272
+ name: 'replacement',
273
+ })
274
+ await tx.mutate.project.update({ id: 'p2', name: 'after' })
275
+ },
276
+ },
277
+ },
278
+ },
279
+ createServerActions: () => ({}),
280
+ transactionProvider,
281
+ })
282
+
283
+ const result = await server.handleMutationRequest({
284
+ authData: null,
285
+ request: pushRequest(1, 'project.converge', {}),
286
+ })
287
+
288
+ expect((result.response as any).mutations[0].result).toEqual({})
289
+ expect(db.prepare('SELECT * FROM project ORDER BY id').all()).toEqual([
290
+ { id: 'p1', ownerId: 'u1', name: 'original' },
291
+ { id: 'p2', ownerId: 'u1', name: 'after' },
292
+ ])
293
+ })
294
+
295
+ test('rolls back row writes and persists an application error with its LMID', async () => {
296
+ const server = createZeroSQLiteServer({
297
+ schema: zeroHttpFixtureSchema,
298
+ models: {
299
+ project: {
300
+ mutate: {
301
+ fail: async ({ tx }, value: Record<string, unknown>) => {
302
+ await tx.mutate.project.insert(value as never)
303
+ throw new Error('rejected')
304
+ },
305
+ },
306
+ },
307
+ },
308
+ createServerActions: () => ({}),
309
+ transactionProvider,
310
+ })
311
+
312
+ const result = await server.handleMutationRequest({
313
+ authData: null,
314
+ request: pushRequest(1, 'project.fail', {
315
+ id: 'p1',
316
+ ownerId: 'u1',
317
+ name: 'first',
318
+ }),
319
+ })
320
+
321
+ expect((result.response as any).mutations[0].result).toMatchObject({
322
+ error: 'app',
323
+ message: 'rejected',
324
+ })
325
+ expect(db.prepare('SELECT * FROM project').all()).toEqual([])
326
+ expect(db.prepare('SELECT lastMutationID FROM zero_0_clients').get()).toEqual({
327
+ lastMutationID: 1,
328
+ })
329
+ const stored = db.prepare('SELECT result FROM zero_0_mutations').get() as {
330
+ result: string
331
+ }
332
+ expect(JSON.parse(stored.result)).toMatchObject({
333
+ error: 'app',
334
+ message: 'rejected',
335
+ })
336
+ })
337
+
338
+ test('allows a custom CRUD mutation to no-op without an automatic permission check', async () => {
339
+ const projectPermission = serverWhere('project', (q) => q.cmp('ownerId', 'u1'))
340
+ const projectMutations = mutations('project', projectPermission, {
341
+ insert: async () => {},
342
+ })
343
+ const server = createZeroSQLiteServer({
344
+ schema: zeroHttpFixtureSchema,
345
+ models: {
346
+ project: { mutate: projectMutations },
347
+ },
348
+ createServerActions: () => ({}),
349
+ transactionProvider,
350
+ })
351
+
352
+ const result = await server.handleMutationRequest({
353
+ authData: { id: 'u1', role: 'user' },
354
+ request: pushRequest(1, 'project.insert', {
355
+ id: 'p1',
356
+ ownerId: 'u1',
357
+ name: 'skip',
358
+ }),
359
+ })
360
+
361
+ expect((result.response as any).mutations[0].result).toEqual({})
362
+ expect(db.prepare('SELECT * FROM project').all()).toEqual([])
363
+ })
364
+
365
+ test('rolls back when a custom CRUD mutation throws', async () => {
366
+ const projectPermission = serverWhere('project', (q) => q.cmp('ownerId', 'u1'))
367
+ const projectMutations = mutations('project', projectPermission, {
368
+ insert: async ({ tx }, value: { id: string; ownerId: string; name: string }) => {
369
+ await tx.mutate.project.insert(value)
370
+ throw new Error('denied by custom validation')
371
+ },
372
+ })
373
+ const server = createZeroSQLiteServer({
374
+ schema: zeroHttpFixtureSchema,
375
+ models: {
376
+ project: { mutate: projectMutations },
377
+ },
378
+ createServerActions: () => ({}),
379
+ transactionProvider,
380
+ })
381
+
382
+ const result = await server.handleMutationRequest({
383
+ authData: { id: 'u2', role: 'user' },
384
+ request: pushRequest(1, 'project.insert', {
385
+ id: 'p2',
386
+ ownerId: 'u2',
387
+ name: 'unauthorized',
388
+ }),
389
+ })
390
+
391
+ expect((result.response as any).mutations[0].result).toMatchObject({
392
+ error: 'app',
393
+ message: 'denied by custom validation',
394
+ })
395
+ expect(db.prepare('SELECT * FROM project').all()).toEqual([])
396
+ })
397
+
398
+ test('permission-checks generated CRUD mutations', async () => {
399
+ let inserted: Record<string, unknown> | undefined
400
+ let permissionChecks = 0
401
+ const projectPermission = serverWhere('project', (q) => q.cmp('ownerId', 'u1'))
402
+ const projectMutations = mutations('project', projectPermission)
403
+ const value = { id: 'p3', ownerId: 'u2', name: 'unauthorized' }
404
+
405
+ await expect(
406
+ projectMutations.insert(
407
+ {
408
+ tx: {
409
+ mutate: {
410
+ project: {
411
+ insert: async (row: Record<string, unknown>) => {
412
+ inserted = row
413
+ },
414
+ },
415
+ },
416
+ },
417
+ can: async (_where, row) => {
418
+ permissionChecks++
419
+ expect(row).toEqual(value)
420
+ throw new Error('denied')
421
+ },
422
+ } as never,
423
+ value,
424
+ ),
425
+ ).rejects.toThrow('denied')
426
+ expect(inserted).toEqual(value)
427
+ expect(permissionChecks).toBe(1)
428
+ })
429
+
430
+ test('recovers duplicate results and deletes them through the cleanup mutation', async () => {
431
+ const server = createZeroSQLiteServer({
432
+ schema: zeroHttpFixtureSchema,
433
+ models: {
434
+ project: {
435
+ mutate: {
436
+ create: async ({ tx }, value: Record<string, unknown>) => {
437
+ await tx.mutate.project.insert(value as never)
438
+ },
439
+ },
440
+ },
441
+ },
442
+ createServerActions: () => ({}),
443
+ transactionProvider,
444
+ })
445
+ const value = {
446
+ id: 'p1',
447
+ ownerId: 'u1',
448
+ name: 'first',
449
+ }
450
+
451
+ await server.handleMutationRequest({
452
+ authData: null,
453
+ request: pushRequest(1, 'project.create', value),
454
+ })
455
+ const duplicate = await server.handleMutationRequest({
456
+ authData: null,
457
+ request: pushRequest(1, 'project.create', value),
458
+ })
459
+
460
+ expect((duplicate.response as any).mutations[0].result).toMatchObject({
461
+ error: 'alreadyProcessed',
462
+ })
463
+ expect(db.prepare('SELECT lastMutationID FROM zero_0_clients').get()).toEqual({
464
+ lastMutationID: 1,
465
+ })
466
+ expect(db.prepare('SELECT COUNT(*) AS count FROM zero_0_mutations').get()).toEqual({
467
+ count: 1,
468
+ })
469
+
470
+ await server.handleMutationRequest({
471
+ authData: null,
472
+ request: cleanupRequest(),
473
+ })
474
+ expect(db.prepare('SELECT COUNT(*) AS count FROM zero_0_mutations').get()).toEqual({
475
+ count: 0,
476
+ })
477
+ })
478
+
479
+ test('supports direct CRUD, server names, value codecs, and query AST execution', async () => {
480
+ const server = createZeroSQLiteServer({
481
+ schema: sqliteTypesSchema,
482
+ models: {
483
+ note: {
484
+ mutate: {
485
+ insert: async ({ tx }, value: Record<string, unknown>) => {
486
+ await tx.mutate.note.insert(value as never)
487
+ },
488
+ upsert: async ({ tx }, value: Record<string, unknown>) => {
489
+ await tx.mutate.note.upsert(value as never)
490
+ },
491
+ update: async ({ tx }, value: Record<string, unknown>) => {
492
+ await tx.mutate.note.update(value as never)
493
+ },
494
+ delete: async ({ tx }, value: Record<string, unknown>) => {
495
+ await tx.mutate.note.delete(value as never)
496
+ },
497
+ },
498
+ },
499
+ },
500
+ createServerActions: () => ({}),
501
+ transactionProvider,
502
+ })
503
+
504
+ await server.mutate.note.insert({
505
+ id: 'n1',
506
+ done: false,
507
+ data: { label: 'first' },
508
+ })
509
+ await server.mutate.note.upsert({
510
+ id: 'n1',
511
+ done: true,
512
+ data: { label: 'second' },
513
+ })
514
+ await server.mutate.note.update({
515
+ id: 'n1',
516
+ done: false,
517
+ })
518
+
519
+ expect(db.prepare('SELECT * FROM note_records').get()).toEqual({
520
+ note_id: 'n1',
521
+ done: 0,
522
+ data: JSON.stringify({ label: 'second' }),
523
+ })
524
+ await expect(server.query((q) => q.note)).resolves.toEqual([
525
+ {
526
+ id: 'n1',
527
+ done: false,
528
+ data: { label: 'second' },
529
+ },
530
+ ])
531
+ await server.transaction(async (tx) => {
532
+ if (tx.location !== 'server') throw new Error('expected server transaction')
533
+ await expect((tx.query as any).note.one().run()).resolves.toEqual({
534
+ id: 'n1',
535
+ done: false,
536
+ data: { label: 'second' },
537
+ })
538
+ await expect(
539
+ tx.dbTransaction.query('SELECT * FROM note_records', []),
540
+ ).resolves.toEqual([
541
+ {
542
+ note_id: 'n1',
543
+ done: 0,
544
+ data: JSON.stringify({ label: 'second' }),
545
+ },
546
+ ])
547
+ })
548
+
549
+ await server.mutate.note.delete({ id: 'n1' })
550
+ expect(db.prepare('SELECT * FROM note_records').all()).toEqual([])
551
+ expect(statements).toEqual([
552
+ { table: 'note_records', publicTable: 'note', kind: 'insert' },
553
+ { table: 'note_records', publicTable: 'note', kind: 'upsert' },
554
+ { table: 'note_records', publicTable: 'note', kind: 'update' },
555
+ { table: 'note_records', publicTable: 'note', kind: 'delete' },
556
+ ])
557
+ })
558
+
559
+ test('decodes destination rows through a hidden two-hop junction', async () => {
560
+ const server = createZeroSQLiteServer({
561
+ schema: twoHopSchema,
562
+ models: {},
563
+ createServerActions: () => ({}),
564
+ transactionProvider,
565
+ })
566
+
567
+ await expect(server.query((q: any) => q.person.related('teams'))).resolves.toEqual([
568
+ {
569
+ id: 'p1',
570
+ teams: [
571
+ {
572
+ id: 't1',
573
+ active: true,
574
+ metadata: { label: 'core' },
575
+ },
576
+ ],
577
+ },
578
+ ])
579
+ })
580
+
581
+ test('accepts a hydrated singular query result from the transaction executor', async () => {
582
+ const hydrated = {
583
+ id: 'n1',
584
+ done: false,
585
+ data: { label: 'already decoded' },
586
+ }
587
+ let receivedQueryName: string | undefined
588
+ const hydratedExecutor = {
589
+ async exec() {
590
+ return { changes: 0 }
591
+ },
592
+ async query() {
593
+ return []
594
+ },
595
+ async queryAst(_ast: unknown, _format: unknown, queryName?: string) {
596
+ receivedQueryName = queryName
597
+ return hydrated
598
+ },
599
+ } as unknown as SQLiteTransactionExecutor
600
+ const hydratedProvider: SQLiteTransactionProvider = async (callback) =>
601
+ callback(hydratedExecutor)
602
+ const server = createZeroSQLiteServer({
603
+ schema: sqliteTypesSchema,
604
+ models: {},
605
+ createServerActions: () => ({}),
606
+ transactionProvider: hydratedProvider,
607
+ })
608
+
609
+ await expect(
610
+ server.query((q) => asQueryInternals(q.note.one()).nameAndArgs('noteByID', ['n1'])),
611
+ ).resolves.toEqual(hydrated)
612
+ expect(receivedQueryName).toBe('noteByID')
613
+ })
614
+ })
@@ -0,0 +1,56 @@
1
+ import { createZeroServerCore } from './createZeroServerCore'
2
+ import {
3
+ createSQLiteDatabase,
4
+ defaultLedgerTables,
5
+ encodeSQLiteValue,
6
+ persistAlreadyProcessedMutationResults,
7
+ type SQLiteLedgerTables,
8
+ type SQLiteTransactionProvider,
9
+ type SQLiteValueContext,
10
+ } from './sqliteRuntime'
11
+
12
+ import type { ZeroServer, ZeroServerCommonOptions } from './createZeroServerCore'
13
+ import type { GenericModels } from './types'
14
+ import type { Schema as ZeroSchema } from '@rocicorp/zero'
15
+
16
+ export type CreateZeroSQLiteServerOptions<
17
+ Schema extends ZeroSchema,
18
+ Models extends GenericModels,
19
+ ServerActions extends Record<string, unknown>,
20
+ > = ZeroServerCommonOptions<Schema, Models, ServerActions> & {
21
+ transactionProvider: SQLiteTransactionProvider
22
+ ledgerTables?: (upstreamSchema: string) => SQLiteLedgerTables
23
+ encodeValue?: (value: unknown, context: SQLiteValueContext) => unknown
24
+ }
25
+
26
+ export function createZeroSQLiteServer<
27
+ Schema extends ZeroSchema,
28
+ Models extends GenericModels,
29
+ ServerActions extends Record<string, unknown>,
30
+ >({
31
+ encodeValue = encodeSQLiteValue,
32
+ ledgerTables = defaultLedgerTables,
33
+ transactionProvider,
34
+ ...options
35
+ }: CreateZeroSQLiteServerOptions<Schema, Models, ServerActions>): ZeroServer<
36
+ Schema,
37
+ Models,
38
+ ServerActions
39
+ > {
40
+ const databaseOptions = {
41
+ encodeValue,
42
+ initializedLedgers: new Set<string>(),
43
+ ledgerTables,
44
+ schema: options.schema,
45
+ transactionProvider,
46
+ }
47
+
48
+ return createZeroServerCore({
49
+ ...options,
50
+ createDatabase: () => createSQLiteDatabase(databaseOptions),
51
+ persistAlreadyProcessed: (args) =>
52
+ persistAlreadyProcessedMutationResults(databaseOptions, args),
53
+ })
54
+ }
55
+
56
+ export * from './sqliteRuntime'