@powersync/service-core 0.2.1 → 0.3.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 (255) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/api/diagnostics.js +2 -2
  3. package/dist/api/diagnostics.js.map +1 -1
  4. package/dist/api/schema.js.map +1 -1
  5. package/dist/auth/CachedKeyCollector.js.map +1 -1
  6. package/dist/auth/KeySpec.js.map +1 -1
  7. package/dist/auth/KeyStore.js +2 -2
  8. package/dist/auth/KeyStore.js.map +1 -1
  9. package/dist/auth/LeakyBucket.js.map +1 -1
  10. package/dist/auth/RemoteJWKSCollector.js.map +1 -1
  11. package/dist/auth/SupabaseKeyCollector.js.map +1 -1
  12. package/dist/db/mongo.js.map +1 -1
  13. package/dist/entry/cli-entry.js +2 -2
  14. package/dist/entry/cli-entry.js.map +1 -1
  15. package/dist/entry/commands/config-command.js.map +1 -1
  16. package/dist/entry/commands/migrate-action.js.map +1 -1
  17. package/dist/entry/commands/start-action.js.map +1 -1
  18. package/dist/entry/commands/teardown-action.js.map +1 -1
  19. package/dist/index.d.ts +3 -2
  20. package/dist/index.js +4 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/locks/LockManager.d.ts +10 -0
  23. package/dist/locks/LockManager.js +7 -0
  24. package/dist/locks/LockManager.js.map +1 -0
  25. package/dist/locks/MongoLocks.d.ts +36 -0
  26. package/dist/locks/MongoLocks.js +81 -0
  27. package/dist/locks/MongoLocks.js.map +1 -0
  28. package/dist/locks/locks-index.d.ts +2 -0
  29. package/dist/locks/locks-index.js +3 -0
  30. package/dist/locks/locks-index.js.map +1 -0
  31. package/dist/metrics/Metrics.js +6 -6
  32. package/dist/metrics/Metrics.js.map +1 -1
  33. package/dist/migrations/db/migrations/1684951997326-init.js.map +1 -1
  34. package/dist/migrations/db/migrations/1702295701188-sync-rule-state.js.map +1 -1
  35. package/dist/migrations/db/migrations/1711543888062-write-checkpoint-index.js.map +1 -1
  36. package/dist/migrations/definitions.d.ts +18 -0
  37. package/dist/migrations/definitions.js +6 -0
  38. package/dist/migrations/definitions.js.map +1 -0
  39. package/dist/migrations/executor.d.ts +16 -0
  40. package/dist/migrations/executor.js +64 -0
  41. package/dist/migrations/executor.js.map +1 -0
  42. package/dist/migrations/migrations-index.d.ts +3 -0
  43. package/dist/migrations/migrations-index.js +4 -0
  44. package/dist/migrations/migrations-index.js.map +1 -0
  45. package/dist/migrations/migrations.d.ts +1 -1
  46. package/dist/migrations/migrations.js +4 -8
  47. package/dist/migrations/migrations.js.map +1 -1
  48. package/dist/migrations/store/migration-store.d.ts +11 -0
  49. package/dist/migrations/store/migration-store.js +46 -0
  50. package/dist/migrations/store/migration-store.js.map +1 -0
  51. package/dist/replication/ErrorRateLimiter.js.map +1 -1
  52. package/dist/replication/PgRelation.js.map +1 -1
  53. package/dist/replication/WalConnection.js.map +1 -1
  54. package/dist/replication/WalStream.d.ts +0 -1
  55. package/dist/replication/WalStream.js +21 -25
  56. package/dist/replication/WalStream.js.map +1 -1
  57. package/dist/replication/WalStreamManager.js +12 -13
  58. package/dist/replication/WalStreamManager.js.map +1 -1
  59. package/dist/replication/WalStreamRunner.js +8 -8
  60. package/dist/replication/WalStreamRunner.js.map +1 -1
  61. package/dist/replication/util.js.map +1 -1
  62. package/dist/routes/auth.d.ts +8 -10
  63. package/dist/routes/auth.js.map +1 -1
  64. package/dist/routes/endpoints/admin.d.ts +1011 -0
  65. package/dist/routes/{admin.js → endpoints/admin.js} +33 -18
  66. package/dist/routes/endpoints/admin.js.map +1 -0
  67. package/dist/routes/endpoints/checkpointing.d.ts +76 -0
  68. package/dist/routes/endpoints/checkpointing.js +36 -0
  69. package/dist/routes/endpoints/checkpointing.js.map +1 -0
  70. package/dist/routes/endpoints/dev.d.ts +312 -0
  71. package/dist/routes/{dev.js → endpoints/dev.js} +25 -16
  72. package/dist/routes/endpoints/dev.js.map +1 -0
  73. package/dist/routes/endpoints/route-endpoints-index.d.ts +6 -0
  74. package/dist/routes/endpoints/route-endpoints-index.js +7 -0
  75. package/dist/routes/endpoints/route-endpoints-index.js.map +1 -0
  76. package/dist/routes/endpoints/socket-route.d.ts +2 -0
  77. package/dist/routes/{socket-route.js → endpoints/socket-route.js} +10 -10
  78. package/dist/routes/endpoints/socket-route.js.map +1 -0
  79. package/dist/routes/endpoints/sync-rules.d.ts +174 -0
  80. package/dist/routes/{sync-rules.js → endpoints/sync-rules.js} +44 -24
  81. package/dist/routes/endpoints/sync-rules.js.map +1 -0
  82. package/dist/routes/endpoints/sync-stream.d.ts +132 -0
  83. package/dist/routes/{sync-stream.js → endpoints/sync-stream.js} +26 -17
  84. package/dist/routes/endpoints/sync-stream.js.map +1 -0
  85. package/dist/routes/hooks.d.ts +10 -0
  86. package/dist/routes/hooks.js +31 -0
  87. package/dist/routes/hooks.js.map +1 -0
  88. package/dist/routes/route-register.d.ts +10 -0
  89. package/dist/routes/route-register.js +87 -0
  90. package/dist/routes/route-register.js.map +1 -0
  91. package/dist/routes/router.d.ts +16 -4
  92. package/dist/routes/router.js +6 -1
  93. package/dist/routes/router.js.map +1 -1
  94. package/dist/routes/routes-index.d.ts +5 -3
  95. package/dist/routes/routes-index.js +5 -3
  96. package/dist/routes/routes-index.js.map +1 -1
  97. package/dist/runner/teardown.js +27 -12
  98. package/dist/runner/teardown.js.map +1 -1
  99. package/dist/storage/BucketStorage.d.ts +3 -0
  100. package/dist/storage/BucketStorage.js.map +1 -1
  101. package/dist/storage/ChecksumCache.js.map +1 -1
  102. package/dist/storage/MongoBucketStorage.js +5 -5
  103. package/dist/storage/MongoBucketStorage.js.map +1 -1
  104. package/dist/storage/SourceTable.js.map +1 -1
  105. package/dist/storage/mongo/MongoBucketBatch.js +23 -18
  106. package/dist/storage/mongo/MongoBucketBatch.js.map +1 -1
  107. package/dist/storage/mongo/MongoIdSequence.js.map +1 -1
  108. package/dist/storage/mongo/MongoSyncBucketStorage.js.map +1 -1
  109. package/dist/storage/mongo/MongoSyncRulesLock.js +3 -3
  110. package/dist/storage/mongo/MongoSyncRulesLock.js.map +1 -1
  111. package/dist/storage/mongo/OperationBatch.js.map +1 -1
  112. package/dist/storage/mongo/PersistedBatch.js +2 -2
  113. package/dist/storage/mongo/PersistedBatch.js.map +1 -1
  114. package/dist/storage/mongo/db.d.ts +2 -2
  115. package/dist/storage/mongo/db.js.map +1 -1
  116. package/dist/storage/mongo/util.js.map +1 -1
  117. package/dist/sync/BroadcastIterable.js.map +1 -1
  118. package/dist/sync/LastValueSink.js.map +1 -1
  119. package/dist/sync/merge.js.map +1 -1
  120. package/dist/sync/safeRace.js.map +1 -1
  121. package/dist/sync/sync.js +4 -4
  122. package/dist/sync/sync.js.map +1 -1
  123. package/dist/sync/util.js.map +1 -1
  124. package/dist/system/CorePowerSyncSystem.d.ts +12 -7
  125. package/dist/system/CorePowerSyncSystem.js +26 -2
  126. package/dist/system/CorePowerSyncSystem.js.map +1 -1
  127. package/dist/system/system-index.d.ts +1 -0
  128. package/dist/system/system-index.js +2 -0
  129. package/dist/system/system-index.js.map +1 -0
  130. package/dist/util/Mutex.js.map +1 -1
  131. package/dist/util/PgManager.js.map +1 -1
  132. package/dist/util/alerting.d.ts +0 -2
  133. package/dist/util/alerting.js +0 -6
  134. package/dist/util/alerting.js.map +1 -1
  135. package/dist/util/config/collectors/config-collector.js +3 -3
  136. package/dist/util/config/collectors/config-collector.js.map +1 -1
  137. package/dist/util/config/collectors/impl/base64-config-collector.js.map +1 -1
  138. package/dist/util/config/collectors/impl/filesystem-config-collector.js +7 -5
  139. package/dist/util/config/collectors/impl/filesystem-config-collector.js.map +1 -1
  140. package/dist/util/config/compound-config-collector.js +4 -4
  141. package/dist/util/config/compound-config-collector.js.map +1 -1
  142. package/dist/util/config/sync-rules/impl/base64-sync-rules-collector.js.map +1 -1
  143. package/dist/util/config/sync-rules/impl/filesystem-sync-rules-collector.js.map +1 -1
  144. package/dist/util/config/sync-rules/impl/inline-sync-rules-collector.js.map +1 -1
  145. package/dist/util/config.js.map +1 -1
  146. package/dist/util/env.d.ts +1 -2
  147. package/dist/util/env.js +3 -2
  148. package/dist/util/env.js.map +1 -1
  149. package/dist/util/memory-tracking.js +2 -2
  150. package/dist/util/memory-tracking.js.map +1 -1
  151. package/dist/util/migration_lib.js.map +1 -1
  152. package/dist/util/pgwire_utils.js +2 -2
  153. package/dist/util/pgwire_utils.js.map +1 -1
  154. package/dist/util/populate_test_data.js.map +1 -1
  155. package/dist/util/secs.js.map +1 -1
  156. package/dist/util/utils.js +4 -4
  157. package/dist/util/utils.js.map +1 -1
  158. package/package.json +13 -10
  159. package/src/api/diagnostics.ts +5 -5
  160. package/src/api/schema.ts +1 -1
  161. package/src/auth/KeyStore.ts +2 -2
  162. package/src/entry/cli-entry.ts +3 -4
  163. package/src/entry/commands/config-command.ts +1 -1
  164. package/src/entry/commands/migrate-action.ts +2 -2
  165. package/src/entry/commands/start-action.ts +1 -1
  166. package/src/entry/commands/teardown-action.ts +1 -1
  167. package/src/index.ts +5 -2
  168. package/src/locks/LockManager.ts +16 -0
  169. package/src/locks/MongoLocks.ts +142 -0
  170. package/src/locks/locks-index.ts +2 -0
  171. package/src/metrics/Metrics.ts +8 -8
  172. package/src/migrations/db/migrations/1684951997326-init.ts +3 -3
  173. package/src/migrations/db/migrations/1702295701188-sync-rule-state.ts +3 -3
  174. package/src/migrations/db/migrations/1711543888062-write-checkpoint-index.ts +2 -2
  175. package/src/migrations/definitions.ts +21 -0
  176. package/src/migrations/executor.ts +87 -0
  177. package/src/migrations/migrations-index.ts +3 -0
  178. package/src/migrations/migrations.ts +7 -11
  179. package/src/migrations/store/migration-store.ts +63 -0
  180. package/src/replication/WalConnection.ts +2 -2
  181. package/src/replication/WalStream.ts +24 -29
  182. package/src/replication/WalStreamManager.ts +14 -15
  183. package/src/replication/WalStreamRunner.ts +10 -10
  184. package/src/replication/util.ts +1 -1
  185. package/src/routes/auth.ts +22 -16
  186. package/src/routes/endpoints/admin.ts +237 -0
  187. package/src/routes/endpoints/checkpointing.ts +41 -0
  188. package/src/routes/endpoints/dev.ts +199 -0
  189. package/src/routes/endpoints/route-endpoints-index.ts +6 -0
  190. package/src/routes/{socket-route.ts → endpoints/socket-route.ts} +11 -11
  191. package/src/routes/endpoints/sync-rules.ts +227 -0
  192. package/src/routes/endpoints/sync-stream.ts +101 -0
  193. package/src/routes/hooks.ts +45 -0
  194. package/src/routes/route-register.ts +104 -0
  195. package/src/routes/router.ts +34 -6
  196. package/src/routes/routes-index.ts +5 -4
  197. package/src/runner/teardown.ts +30 -13
  198. package/src/storage/BucketStorage.ts +7 -2
  199. package/src/storage/ChecksumCache.ts +2 -2
  200. package/src/storage/MongoBucketStorage.ts +8 -8
  201. package/src/storage/SourceTable.ts +2 -2
  202. package/src/storage/mongo/MongoBucketBatch.ts +29 -22
  203. package/src/storage/mongo/MongoSyncBucketStorage.ts +3 -3
  204. package/src/storage/mongo/MongoSyncRulesLock.ts +3 -3
  205. package/src/storage/mongo/OperationBatch.ts +1 -1
  206. package/src/storage/mongo/PersistedBatch.ts +3 -3
  207. package/src/storage/mongo/db.ts +3 -4
  208. package/src/sync/sync.ts +8 -8
  209. package/src/sync/util.ts +2 -2
  210. package/src/system/CorePowerSyncSystem.ts +31 -10
  211. package/src/system/system-index.ts +1 -0
  212. package/src/util/alerting.ts +0 -8
  213. package/src/util/config/collectors/config-collector.ts +5 -3
  214. package/src/util/config/collectors/impl/filesystem-config-collector.ts +8 -6
  215. package/src/util/config/compound-config-collector.ts +4 -4
  216. package/src/util/env.ts +4 -2
  217. package/src/util/memory-tracking.ts +2 -2
  218. package/src/util/pgwire_utils.ts +3 -3
  219. package/src/util/utils.ts +5 -5
  220. package/test/src/auth.test.ts +4 -2
  221. package/test/src/data_storage.test.ts +177 -0
  222. package/test/src/env.ts +6 -6
  223. package/test/src/pg_test.test.ts +18 -0
  224. package/test/src/setup.ts +7 -0
  225. package/test/src/slow_tests.test.ts +45 -6
  226. package/test/tsconfig.json +1 -1
  227. package/tsconfig.json +5 -6
  228. package/tsconfig.tsbuildinfo +1 -1
  229. package/vitest.config.ts +1 -3
  230. package/dist/migrations/db/store.d.ts +0 -3
  231. package/dist/migrations/db/store.js +0 -10
  232. package/dist/migrations/db/store.js.map +0 -1
  233. package/dist/routes/admin.d.ts +0 -7
  234. package/dist/routes/admin.js.map +0 -1
  235. package/dist/routes/checkpointing.d.ts +0 -3
  236. package/dist/routes/checkpointing.js +0 -30
  237. package/dist/routes/checkpointing.js.map +0 -1
  238. package/dist/routes/dev.d.ts +0 -6
  239. package/dist/routes/dev.js.map +0 -1
  240. package/dist/routes/route-generators.d.ts +0 -15
  241. package/dist/routes/route-generators.js +0 -32
  242. package/dist/routes/route-generators.js.map +0 -1
  243. package/dist/routes/socket-route.d.ts +0 -2
  244. package/dist/routes/socket-route.js.map +0 -1
  245. package/dist/routes/sync-rules.d.ts +0 -6
  246. package/dist/routes/sync-rules.js.map +0 -1
  247. package/dist/routes/sync-stream.d.ts +0 -5
  248. package/dist/routes/sync-stream.js.map +0 -1
  249. package/src/migrations/db/store.ts +0 -11
  250. package/src/routes/admin.ts +0 -229
  251. package/src/routes/checkpointing.ts +0 -38
  252. package/src/routes/dev.ts +0 -194
  253. package/src/routes/route-generators.ts +0 -39
  254. package/src/routes/sync-rules.ts +0 -210
  255. package/src/routes/sync-stream.ts +0 -95
@@ -0,0 +1,199 @@
1
+ import * as t from 'ts-codec';
2
+ import * as pgwire from '@powersync/service-jpgwire';
3
+ import { errors, router, schema } from '@powersync/lib-services-framework';
4
+
5
+ import * as util from '../../util/util-index.js';
6
+ import { authDevUser, authUser, endpoint, issueDevToken, issueLegacyDevToken, issuePowerSyncToken } from '../auth.js';
7
+ import { routeDefinition } from '../router.js';
8
+
9
+ const AuthParams = t.object({
10
+ user: t.string,
11
+ password: t.string
12
+ });
13
+
14
+ // For legacy web client only. Remove soon.
15
+ export const auth = routeDefinition({
16
+ path: '/auth.json',
17
+ method: router.HTTPMethod.POST,
18
+ validator: schema.createTsCodecValidator(AuthParams, { allowAdditional: true }),
19
+ handler: async (payload) => {
20
+ const { user, password } = payload.params;
21
+ const config = payload.context.system.config;
22
+
23
+ if (config.dev.demo_auth == false || config.dev.demo_password == null) {
24
+ throw new errors.AuthorizationError(['Demo auth disabled']);
25
+ }
26
+
27
+ if (password == config.dev.demo_password) {
28
+ const token = await issueLegacyDevToken(payload.request, user, payload.context.system.config);
29
+ return { token, user_id: user, endpoint: endpoint(payload.request) };
30
+ } else {
31
+ throw new errors.AuthorizationError(['Authentication failed']);
32
+ }
33
+ }
34
+ });
35
+
36
+ export const auth2 = routeDefinition({
37
+ path: '/dev/auth.json',
38
+ method: router.HTTPMethod.POST,
39
+ validator: schema.createTsCodecValidator(AuthParams, { allowAdditional: true }),
40
+ handler: async (payload) => {
41
+ const { user, password } = payload.params;
42
+ const config = payload.context.system.config;
43
+
44
+ if (config.dev.demo_auth == false || config.dev.demo_password == null) {
45
+ throw new errors.AuthorizationError(['Demo auth disabled']);
46
+ }
47
+
48
+ if (password == config.dev.demo_password) {
49
+ const token = await issueDevToken(payload.request, user, payload.context.system.config);
50
+ return { token, user_id: user };
51
+ } else {
52
+ throw new errors.AuthorizationError(['Authentication failed']);
53
+ }
54
+ }
55
+ });
56
+
57
+ const TokenParams = t.object({});
58
+
59
+ export const token = routeDefinition({
60
+ path: '/dev/token.json',
61
+ method: router.HTTPMethod.POST,
62
+ validator: schema.createTsCodecValidator(TokenParams, { allowAdditional: true }),
63
+ authorize: authDevUser,
64
+ handler: async (payload) => {
65
+ const { user_id } = payload.context;
66
+ const outToken = await issuePowerSyncToken(payload.request, user_id!, payload.context.system.config);
67
+ return { token: outToken, user_id: user_id, endpoint: endpoint(payload.request) };
68
+ }
69
+ });
70
+
71
+ const OpType = {
72
+ PUT: 'PUT',
73
+ PATCH: 'PATCH',
74
+ DELETE: 'DELETE'
75
+ };
76
+
77
+ const CrudEntry = t.object({
78
+ op: t.Enum(OpType),
79
+ type: t.string,
80
+ id: t.string,
81
+ op_id: t.number.optional(),
82
+ data: t.any.optional()
83
+ });
84
+
85
+ const CrudRequest = t.object({
86
+ data: t.array(CrudEntry),
87
+ write_checkpoint: t.boolean.optional()
88
+ });
89
+
90
+ export const crud = routeDefinition({
91
+ path: '/crud.json',
92
+ method: router.HTTPMethod.POST,
93
+ validator: schema.createTsCodecValidator(CrudRequest, { allowAdditional: true }),
94
+ authorize: authUser,
95
+
96
+ handler: async (payload) => {
97
+ const { user_id, system } = payload.context;
98
+
99
+ const pool = system.requirePgPool();
100
+
101
+ if (!system.config.dev.crud_api) {
102
+ throw new Error('CRUD api disabled');
103
+ }
104
+
105
+ const params = payload.params;
106
+
107
+ let statements: pgwire.Statement[] = [];
108
+
109
+ // Implementation note:
110
+ // Postgres does automatic "assigment cast" for query literals,
111
+ // e.g. a string literal to uuid. However, the same doesn't apply
112
+ // to query parameters.
113
+ // To handle those automatically, we use `json_populate_record`
114
+ // to automatically cast to the correct types.
115
+
116
+ for (let op of params.data) {
117
+ const table = util.escapeIdentifier(op.type);
118
+ if (op.op == 'PUT') {
119
+ const data = op.data as Record<string, any>;
120
+ const with_id = { ...data, id: op.id };
121
+
122
+ const columnsEscaped = Object.keys(with_id).map(util.escapeIdentifier);
123
+ const columnsJoined = columnsEscaped.join(', ');
124
+
125
+ let updateClauses: string[] = [];
126
+
127
+ for (let key of Object.keys(data)) {
128
+ updateClauses.push(`${util.escapeIdentifier(key)} = EXCLUDED.${util.escapeIdentifier(key)}`);
129
+ }
130
+
131
+ const updateClause = updateClauses.length > 0 ? `DO UPDATE SET ${updateClauses.join(', ')}` : `DO NOTHING`;
132
+
133
+ const statement = `
134
+ WITH data_row AS (
135
+ SELECT (json_populate_record(null::${table}, $1::json)).*
136
+ )
137
+ INSERT INTO ${table} (${columnsJoined})
138
+ SELECT ${columnsJoined} FROM data_row
139
+ ON CONFLICT(id) ${updateClause}`;
140
+
141
+ statements.push({
142
+ statement: statement,
143
+ params: [{ type: 'varchar', value: JSON.stringify(with_id) }]
144
+ });
145
+ } else if (op.op == 'PATCH') {
146
+ const data = op.data as Record<string, any>;
147
+ const with_id = { ...data, id: op.id };
148
+
149
+ let updateClauses: string[] = [];
150
+
151
+ for (let key of Object.keys(data)) {
152
+ updateClauses.push(`${util.escapeIdentifier(key)} = data_row.${util.escapeIdentifier(key)}`);
153
+ }
154
+
155
+ const statement = `
156
+ WITH data_row AS (
157
+ SELECT (json_populate_record(null::${table}, $1::json)).*
158
+ )
159
+ UPDATE ${table}
160
+ SET ${updateClauses.join(', ')}
161
+ FROM data_row
162
+ WHERE ${table}.id = data_row.id`;
163
+
164
+ statements.push({
165
+ statement: statement,
166
+ params: [{ type: 'varchar', value: JSON.stringify(with_id) }]
167
+ });
168
+ } else if (op.op == 'DELETE') {
169
+ statements.push({
170
+ statement: `
171
+ WITH data_row AS (
172
+ SELECT (json_populate_record(null::${table}, $1::json)).*
173
+ )
174
+ DELETE FROM ${table}
175
+ USING data_row
176
+ WHERE ${table}.id = data_row.id`,
177
+ params: [{ type: 'varchar', value: JSON.stringify({ id: op.id }) }]
178
+ });
179
+ }
180
+ }
181
+ await pool.query(...statements);
182
+
183
+ const storage = system.storage;
184
+ if (payload.params.write_checkpoint === true) {
185
+ const write_checkpoint = await util.createWriteCheckpoint(pool, storage, payload.context.user_id!);
186
+ return { write_checkpoint: String(write_checkpoint) };
187
+ } else if (payload.params.write_checkpoint === false) {
188
+ return {};
189
+ } else {
190
+ // Legacy
191
+ const checkpoint = await util.getClientCheckpoint(pool, storage);
192
+ return {
193
+ checkpoint
194
+ };
195
+ }
196
+ }
197
+ });
198
+
199
+ export const DEV_ROUTES = [auth, auth2, token, crud];
@@ -0,0 +1,6 @@
1
+ export * from './admin.js';
2
+ export * from './checkpointing.js';
3
+ export * from './dev.js';
4
+ export * from './socket-route.js';
5
+ export * from './sync-rules.js';
6
+ export * from './sync-stream.js';
@@ -1,14 +1,14 @@
1
1
  import { serialize } from 'bson';
2
2
  import { SyncParameters, normalizeTokenParameters } from '@powersync/service-sync-rules';
3
- import * as micro from '@journeyapps-platform/micro';
3
+ import { errors, logger, schema } from '@powersync/lib-services-framework';
4
4
 
5
- import * as util from '@/util/util-index.js';
6
- import { streamResponse } from '../sync/sync.js';
5
+ import * as util from '../../util/util-index.js';
6
+ import { streamResponse } from '../../sync/sync.js';
7
7
  import { SyncRoutes } from './sync-stream.js';
8
- import { SocketRouteGenerator } from './router-socket.js';
9
- import { Metrics } from '@/metrics/Metrics.js';
8
+ import { SocketRouteGenerator } from '../router-socket.js';
9
+ import { Metrics } from '../../metrics/Metrics.js';
10
10
 
11
- export const sync_stream_reactive: SocketRouteGenerator = (router) =>
11
+ export const syncStreamReactive: SocketRouteGenerator = (router) =>
12
12
  router.reactiveStream<util.StreamingSyncRequest, any>(SyncRoutes.STREAM, {
13
13
  authorize: ({ context }) => {
14
14
  return {
@@ -16,13 +16,13 @@ export const sync_stream_reactive: SocketRouteGenerator = (router) =>
16
16
  errors: ['Authentication required'].concat(context.token_errors ?? [])
17
17
  };
18
18
  },
19
- validator: micro.schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
19
+ validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
20
20
  handler: async ({ context, params, responder, observer, initialN }) => {
21
21
  const { system } = context;
22
22
 
23
23
  if (system.closed) {
24
24
  responder.onError(
25
- new micro.errors.JourneyError({
25
+ new errors.JourneyError({
26
26
  status: 503,
27
27
  code: 'SERVICE_UNAVAILABLE',
28
28
  description: 'Service temporarily unavailable'
@@ -44,7 +44,7 @@ export const sync_stream_reactive: SocketRouteGenerator = (router) =>
44
44
  const cp = await storage.getActiveCheckpoint();
45
45
  if (!cp.hasSyncRules()) {
46
46
  responder.onError(
47
- new micro.errors.JourneyError({
47
+ new errors.JourneyError({
48
48
  status: 500,
49
49
  code: 'NO_SYNC_RULES',
50
50
  description: 'No sync rules available'
@@ -122,8 +122,8 @@ export const sync_stream_reactive: SocketRouteGenerator = (router) =>
122
122
  } catch (ex) {
123
123
  // Convert to our standard form before responding.
124
124
  // This ensures the error can be serialized.
125
- const error = new micro.errors.InternalServerError(ex);
126
- micro.logger.error('Sync stream error', error);
125
+ const error = new errors.InternalServerError(ex);
126
+ logger.error('Sync stream error', error);
127
127
  responder.onError(error);
128
128
  } finally {
129
129
  responder.onComplete();
@@ -0,0 +1,227 @@
1
+ import * as t from 'ts-codec';
2
+ import type { FastifyPluginAsync } from 'fastify';
3
+ import * as pgwire from '@powersync/service-jpgwire';
4
+ import { errors, router, schema } from '@powersync/lib-services-framework';
5
+ import { SqlSyncRules, SyncRulesErrors } from '@powersync/service-sync-rules';
6
+
7
+ import * as replication from '../../replication/replication-index.js';
8
+ import { authApi } from '../auth.js';
9
+ import { routeDefinition } from '../router.js';
10
+
11
+ const DeploySyncRulesRequest = t.object({
12
+ content: t.string
13
+ });
14
+
15
+ export const yamlPlugin: FastifyPluginAsync = async (fastify) => {
16
+ fastify.addContentTypeParser('application/yaml', async (request, payload, _d) => {
17
+ const data: any[] = [];
18
+ for await (const chunk of payload) {
19
+ data.push(chunk);
20
+ }
21
+
22
+ request.params = { content: Buffer.concat(data).toString('utf8') };
23
+ });
24
+ };
25
+
26
+ /**
27
+ * Declares the plugin should be available on the same scope
28
+ * without requiring the `fastify-plugin` package as a dependency.
29
+ * https://fastify.dev/docs/latest/Reference/Plugins/#handle-the-scope
30
+ */
31
+ //@ts-expect-error
32
+ yamlPlugin[Symbol.for('skip-override')] = true;
33
+
34
+ export const deploySyncRules = routeDefinition({
35
+ path: '/api/sync-rules/v1/deploy',
36
+ method: router.HTTPMethod.POST,
37
+ authorize: authApi,
38
+ parse: true,
39
+ plugins: [yamlPlugin],
40
+ validator: schema.createTsCodecValidator(DeploySyncRulesRequest, { allowAdditional: true }),
41
+ handler: async (payload) => {
42
+ if (payload.context.system.config.sync_rules.present) {
43
+ // If sync rules are configured via the config, disable deploy via the API.
44
+ throw new errors.JourneyError({
45
+ status: 422,
46
+ code: 'API_DISABLED',
47
+ description: 'Sync rules API disabled',
48
+ details: 'Use the management API to deploy sync rules'
49
+ });
50
+ }
51
+ const content = payload.params.content;
52
+
53
+ try {
54
+ SqlSyncRules.fromYaml(payload.params.content);
55
+ } catch (e) {
56
+ throw new errors.JourneyError({
57
+ status: 422,
58
+ code: 'INVALID_SYNC_RULES',
59
+ description: 'Sync rules parsing failed',
60
+ details: e.message
61
+ });
62
+ }
63
+
64
+ const sync_rules = await payload.context.system.storage.updateSyncRules({
65
+ content: content
66
+ });
67
+
68
+ return {
69
+ slot_name: sync_rules.slot_name
70
+ };
71
+ }
72
+ });
73
+
74
+ const ValidateSyncRulesRequest = t.object({
75
+ content: t.string
76
+ });
77
+
78
+ export const validateSyncRules = routeDefinition({
79
+ path: '/api/sync-rules/v1/validate',
80
+ method: router.HTTPMethod.POST,
81
+ authorize: authApi,
82
+ parse: true,
83
+ plugins: [yamlPlugin],
84
+ validator: schema.createTsCodecValidator(ValidateSyncRulesRequest, { allowAdditional: true }),
85
+ handler: async (payload) => {
86
+ const content = payload.params.content;
87
+
88
+ const info = await debugSyncRules(payload.context.system.requirePgPool(), content);
89
+
90
+ return replyPrettyJson(info);
91
+ }
92
+ });
93
+
94
+ export const currentSyncRules = routeDefinition({
95
+ path: '/api/sync-rules/v1/current',
96
+ method: router.HTTPMethod.GET,
97
+ authorize: authApi,
98
+ handler: async (payload) => {
99
+ const storage = payload.context.system.storage;
100
+ const sync_rules = await storage.getActiveSyncRulesContent();
101
+ if (!sync_rules) {
102
+ throw new errors.JourneyError({
103
+ status: 422,
104
+ code: 'NO_SYNC_RULES',
105
+ description: 'No active sync rules'
106
+ });
107
+ }
108
+ const info = await debugSyncRules(payload.context.system.requirePgPool(), sync_rules.sync_rules_content);
109
+ const next = await storage.getNextSyncRulesContent();
110
+
111
+ const next_info = next
112
+ ? await debugSyncRules(payload.context.system.requirePgPool(), next.sync_rules_content)
113
+ : null;
114
+
115
+ const response = {
116
+ current: {
117
+ slot_name: sync_rules.slot_name,
118
+ content: sync_rules.sync_rules_content,
119
+ ...info
120
+ },
121
+ next:
122
+ next == null
123
+ ? null
124
+ : {
125
+ slot_name: next.slot_name,
126
+ content: next.sync_rules_content,
127
+ ...next_info
128
+ }
129
+ };
130
+
131
+ return replyPrettyJson({ data: response });
132
+ }
133
+ });
134
+
135
+ const ReprocessSyncRulesRequest = t.object({});
136
+
137
+ export const reprocessSyncRules = routeDefinition({
138
+ path: '/api/sync-rules/v1/reprocess',
139
+ method: router.HTTPMethod.POST,
140
+ authorize: authApi,
141
+ validator: schema.createTsCodecValidator(ReprocessSyncRulesRequest),
142
+ handler: async (payload) => {
143
+ const storage = payload.context.system.storage;
144
+ const sync_rules = await storage.getActiveSyncRules();
145
+ if (sync_rules == null) {
146
+ throw new errors.JourneyError({
147
+ status: 422,
148
+ code: 'NO_SYNC_RULES',
149
+ description: 'No active sync rules'
150
+ });
151
+ }
152
+
153
+ const new_rules = await storage.updateSyncRules({
154
+ content: sync_rules.sync_rules.content
155
+ });
156
+ return {
157
+ slot_name: new_rules.slot_name
158
+ };
159
+ }
160
+ });
161
+
162
+ export const SYNC_RULES_ROUTES = [validateSyncRules, deploySyncRules, reprocessSyncRules, currentSyncRules];
163
+
164
+ function replyPrettyJson(payload: any) {
165
+ return new router.RouterResponse({
166
+ status: 200,
167
+ data: JSON.stringify(payload, null, 2) + '\n',
168
+ headers: { 'Content-Type': 'application/json' }
169
+ });
170
+ }
171
+
172
+ async function debugSyncRules(db: pgwire.PgClient, sync_rules: string) {
173
+ try {
174
+ const rules = SqlSyncRules.fromYaml(sync_rules);
175
+ const source_table_patterns = rules.getSourceTables();
176
+ const wc = new replication.WalConnection({
177
+ db: db,
178
+ sync_rules: rules
179
+ });
180
+ const resolved_tables = await wc.getDebugTablesInfo(source_table_patterns);
181
+
182
+ return {
183
+ valid: true,
184
+ bucket_definitions: rules.bucket_descriptors.map((d) => {
185
+ let all_parameter_queries = [...d.parameter_queries.values()].flat();
186
+ let all_data_queries = [...d.data_queries.values()].flat();
187
+ return {
188
+ name: d.name,
189
+ bucket_parameters: d.bucket_parameters,
190
+ global_parameter_queries: d.global_parameter_queries.map((q) => {
191
+ return {
192
+ sql: q.sql
193
+ };
194
+ }),
195
+ parameter_queries: all_parameter_queries.map((q) => {
196
+ return {
197
+ sql: q.sql,
198
+ table: q.sourceTable,
199
+ input_parameters: q.input_parameters
200
+ };
201
+ }),
202
+
203
+ data_queries: all_data_queries.map((q) => {
204
+ return {
205
+ sql: q.sql,
206
+ table: q.sourceTable,
207
+ columns: q.columnOutputNames()
208
+ };
209
+ })
210
+ };
211
+ }),
212
+ source_tables: resolved_tables,
213
+ data_tables: rules.debugGetOutputTables()
214
+ };
215
+ } catch (e) {
216
+ if (e instanceof SyncRulesErrors) {
217
+ return {
218
+ valid: false,
219
+ errors: e.errors.map((e) => e.message)
220
+ };
221
+ }
222
+ return {
223
+ valid: false,
224
+ errors: [e.message]
225
+ };
226
+ }
227
+ }
@@ -0,0 +1,101 @@
1
+ import { Readable } from 'stream';
2
+ import { SyncParameters, normalizeTokenParameters } from '@powersync/service-sync-rules';
3
+ import { errors, logger, router, schema } from '@powersync/lib-services-framework';
4
+
5
+ import * as sync from '../../sync/sync-index.js';
6
+ import * as util from '../../util/util-index.js';
7
+
8
+ import { authUser } from '../auth.js';
9
+ import { routeDefinition } from '../router.js';
10
+ import { Metrics } from '../../metrics/Metrics.js';
11
+
12
+ export enum SyncRoutes {
13
+ STREAM = '/sync/stream'
14
+ }
15
+
16
+ export const syncStreamed = routeDefinition({
17
+ path: SyncRoutes.STREAM,
18
+ method: router.HTTPMethod.POST,
19
+ authorize: authUser,
20
+ validator: schema.createTsCodecValidator(util.StreamingSyncRequest, { allowAdditional: true }),
21
+ handler: async (payload) => {
22
+ const system = payload.context.system;
23
+
24
+ if (system.closed) {
25
+ throw new errors.JourneyError({
26
+ status: 503,
27
+ code: 'SERVICE_UNAVAILABLE',
28
+ description: 'Service temporarily unavailable'
29
+ });
30
+ }
31
+
32
+ const params: util.StreamingSyncRequest = payload.params;
33
+ const syncParams: SyncParameters = normalizeTokenParameters(
34
+ payload.context.token_payload!.parameters ?? {},
35
+ payload.params.parameters ?? {}
36
+ );
37
+
38
+ const storage = system.storage;
39
+ // Sanity check before we start the stream
40
+ const cp = await storage.getActiveCheckpoint();
41
+ if (!cp.hasSyncRules()) {
42
+ throw new errors.JourneyError({
43
+ status: 500,
44
+ code: 'NO_SYNC_RULES',
45
+ description: 'No sync rules available'
46
+ });
47
+ }
48
+ const controller = new AbortController();
49
+ try {
50
+ Metrics.getInstance().concurrent_connections.add(1);
51
+ const stream = Readable.from(
52
+ sync.transformToBytesTracked(
53
+ sync.ndjson(
54
+ sync.streamResponse({
55
+ storage,
56
+ params,
57
+ syncParams,
58
+ token: payload.context.token_payload!,
59
+ signal: controller.signal
60
+ })
61
+ )
62
+ ),
63
+ { objectMode: false, highWaterMark: 16 * 1024 }
64
+ );
65
+
66
+ const deregister = system.addStopHandler(() => {
67
+ // This error is not currently propagated to the client
68
+ controller.abort();
69
+ stream.destroy(new Error('Shutting down system'));
70
+ });
71
+ stream.on('close', () => {
72
+ deregister();
73
+ });
74
+
75
+ stream.on('error', (error) => {
76
+ controller.abort();
77
+ // Note: This appears as a 200 response in the logs.
78
+ if (error.message != 'Shutting down system') {
79
+ logger.error('Streaming sync request failed', error);
80
+ }
81
+ });
82
+
83
+ return new router.RouterResponse({
84
+ status: 200,
85
+ headers: {
86
+ 'Content-Type': 'application/x-ndjson'
87
+ },
88
+ data: stream,
89
+ afterSend: async () => {
90
+ controller.abort();
91
+ Metrics.getInstance().concurrent_connections.add(-1);
92
+ }
93
+ });
94
+ } catch (ex) {
95
+ controller.abort();
96
+ Metrics.getInstance().concurrent_connections.add(-1);
97
+ }
98
+ }
99
+ });
100
+
101
+ export const SYNC_STREAM_ROUTES = [syncStreamed];
@@ -0,0 +1,45 @@
1
+ import type fastify from 'fastify';
2
+ import a from 'async';
3
+ import { logger } from '@powersync/lib-services-framework';
4
+
5
+ export type CreateRequestQueueParams = {
6
+ max_queue_depth: number;
7
+ concurrency: number;
8
+ };
9
+
10
+ /**
11
+ * Creates a request queue which limits the amount of concurrent connections which
12
+ * are active at any time.
13
+ */
14
+ export const createRequestQueueHook = (params: CreateRequestQueueParams): fastify.onRequestHookHandler => {
15
+ const request_queue = a.queue<() => Promise<void>>((event, done) => {
16
+ event().finally(done);
17
+ }, params.concurrency);
18
+
19
+ return (request, reply, next) => {
20
+ if (
21
+ (params.max_queue_depth == 0 && request_queue.running() == params.concurrency) ||
22
+ (params.max_queue_depth > 0 && request_queue.length() >= params.max_queue_depth)
23
+ ) {
24
+ logger.warn(`${request.method} ${request.url}`, {
25
+ status: 429,
26
+ method: request.method,
27
+ path: request.url,
28
+ queue_overflow: true
29
+ });
30
+ return reply.status(429).send();
31
+ }
32
+
33
+ const finished = new Promise<void>((resolve) => {
34
+ reply.then(
35
+ () => resolve(),
36
+ () => resolve()
37
+ );
38
+ });
39
+
40
+ request_queue.push(() => {
41
+ next();
42
+ return finished;
43
+ });
44
+ };
45
+ };