@atproto/bsync 0.0.18 → 0.0.20

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 (68) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE.txt +1 -1
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/context.d.ts +2 -0
  6. package/dist/context.d.ts.map +1 -1
  7. package/dist/context.js +1 -0
  8. package/dist/context.js.map +1 -1
  9. package/dist/db/index.js +17 -7
  10. package/dist/db/index.js.map +1 -1
  11. package/dist/db/migrations/20250527T022203400Z-add-operation.d.ts +4 -0
  12. package/dist/db/migrations/20250527T022203400Z-add-operation.d.ts.map +1 -0
  13. package/dist/db/migrations/20250527T022203400Z-add-operation.js +21 -0
  14. package/dist/db/migrations/20250527T022203400Z-add-operation.js.map +1 -0
  15. package/dist/db/migrations/index.d.ts +1 -0
  16. package/dist/db/migrations/index.d.ts.map +1 -1
  17. package/dist/db/migrations/index.js +19 -8
  18. package/dist/db/migrations/index.js.map +1 -1
  19. package/dist/db/schema/index.d.ts +2 -1
  20. package/dist/db/schema/index.d.ts.map +1 -1
  21. package/dist/db/schema/operation.d.ts +18 -0
  22. package/dist/db/schema/operation.d.ts.map +1 -0
  23. package/dist/db/schema/operation.js +6 -0
  24. package/dist/db/schema/operation.js.map +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +5 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/proto/bsync_connect.d.ts +19 -1
  29. package/dist/proto/bsync_connect.d.ts.map +1 -1
  30. package/dist/proto/bsync_connect.js +18 -0
  31. package/dist/proto/bsync_connect.js.map +1 -1
  32. package/dist/proto/bsync_pb.d.ts +150 -0
  33. package/dist/proto/bsync_pb.d.ts.map +1 -1
  34. package/dist/proto/bsync_pb.js +401 -1
  35. package/dist/proto/bsync_pb.js.map +1 -1
  36. package/dist/routes/add-mute-operation.d.ts.map +1 -1
  37. package/dist/routes/add-notif-operation.d.ts.map +1 -1
  38. package/dist/routes/auth.d.ts.map +1 -1
  39. package/dist/routes/index.d.ts.map +1 -1
  40. package/dist/routes/index.js +4 -0
  41. package/dist/routes/index.js.map +1 -1
  42. package/dist/routes/put-operation.d.ts +6 -0
  43. package/dist/routes/put-operation.d.ts.map +1 -0
  44. package/dist/routes/put-operation.js +72 -0
  45. package/dist/routes/put-operation.js.map +1 -0
  46. package/dist/routes/scan-mute-operations.d.ts.map +1 -1
  47. package/dist/routes/scan-notif-operations.d.ts.map +1 -1
  48. package/dist/routes/scan-operations.d.ts +6 -0
  49. package/dist/routes/scan-operations.d.ts.map +1 -0
  50. package/dist/routes/scan-operations.js +59 -0
  51. package/dist/routes/scan-operations.js.map +1 -0
  52. package/dist/routes/util.d.ts.map +1 -1
  53. package/package.json +2 -2
  54. package/proto/bsync.proto +40 -0
  55. package/src/context.ts +2 -0
  56. package/src/db/migrations/20250527T022203400Z-add-operation.ts +20 -0
  57. package/src/db/migrations/index.ts +1 -0
  58. package/src/db/schema/index.ts +3 -1
  59. package/src/db/schema/operation.ts +20 -0
  60. package/src/index.ts +5 -0
  61. package/src/proto/bsync_connect.ts +22 -0
  62. package/src/proto/bsync_pb.ts +355 -0
  63. package/src/routes/index.ts +4 -0
  64. package/src/routes/put-operation.ts +101 -0
  65. package/src/routes/scan-operations.ts +67 -0
  66. package/tests/operations.test.ts +295 -0
  67. package/tsconfig.build.tsbuildinfo +1 -1
  68. package/tsconfig.tests.tsbuildinfo +1 -1
@@ -0,0 +1,295 @@
1
+ import assert from 'node:assert'
2
+ import { Code, ConnectError } from '@connectrpc/connect'
3
+ import getPort from 'get-port'
4
+ import { wait } from '@atproto/common'
5
+ import {
6
+ BsyncClient,
7
+ BsyncService,
8
+ Database,
9
+ authWithApiKey,
10
+ createClient,
11
+ envToCfg,
12
+ } from '../src'
13
+ import { Method, Operation } from '../src/proto/bsync_pb'
14
+
15
+ describe('operations', () => {
16
+ let bsync: BsyncService
17
+ let client: BsyncClient
18
+
19
+ beforeAll(async () => {
20
+ bsync = await BsyncService.create(
21
+ envToCfg({
22
+ port: await getPort(),
23
+ dbUrl: process.env.DB_POSTGRES_URL,
24
+ dbSchema: 'bsync_operations',
25
+ apiKeys: ['key-1'],
26
+ longPollTimeoutMs: 500,
27
+ }),
28
+ )
29
+ await bsync.ctx.db.migrateToLatestOrThrow()
30
+ await bsync.start()
31
+ client = createClient({
32
+ httpVersion: '1.1',
33
+ baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,
34
+ interceptors: [authWithApiKey('key-1')],
35
+ })
36
+ })
37
+
38
+ afterAll(async () => {
39
+ await bsync.destroy()
40
+ })
41
+
42
+ beforeEach(async () => {
43
+ await clearOps(bsync.ctx.db)
44
+ })
45
+
46
+ describe('putOperation', () => {
47
+ it('requires auth.', async () => {
48
+ // unauthed
49
+ const unauthedClient = createClient({
50
+ httpVersion: '1.1',
51
+ baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,
52
+ })
53
+ const tryPutOperation1 = unauthedClient.putOperation({
54
+ collection: 'app.bsky.some.col',
55
+ actorDid: 'did:example:a',
56
+ rkey: 'rkey1',
57
+ method: Method.CREATE,
58
+ payload: Buffer.from([1, 2, 3]),
59
+ })
60
+ await expect(tryPutOperation1).rejects.toEqual(
61
+ new ConnectError('missing auth', Code.Unauthenticated),
62
+ )
63
+ // bad auth
64
+ const badauthedClient = createClient({
65
+ httpVersion: '1.1',
66
+ baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,
67
+ interceptors: [authWithApiKey('key-bad')],
68
+ })
69
+ const tryPutOperation2 = badauthedClient.putOperation({
70
+ collection: 'app.bsky.some.col',
71
+ actorDid: 'did:example:a',
72
+ rkey: 'rkey1',
73
+ method: Method.CREATE,
74
+ payload: Buffer.from([1, 2, 3]),
75
+ })
76
+ await expect(tryPutOperation2).rejects.toEqual(
77
+ new ConnectError('invalid api key', Code.Unauthenticated),
78
+ )
79
+ })
80
+
81
+ it('fails on bad inputs.', async () => {
82
+ await expect(
83
+ client.putOperation({
84
+ collection: 'bad-collection',
85
+ actorDid: 'did:example:a',
86
+ rkey: 'rkey1',
87
+ method: Method.CREATE,
88
+ payload: Buffer.from([]),
89
+ }),
90
+ ).rejects.toEqual(
91
+ new ConnectError(
92
+ 'operation collection is invalid NSID',
93
+ Code.InvalidArgument,
94
+ ),
95
+ )
96
+ await expect(
97
+ client.putOperation({
98
+ collection: 'app.bsky.some.col',
99
+ actorDid: 'bad-did',
100
+ rkey: 'rkey1',
101
+ method: Method.CREATE,
102
+ payload: Buffer.from([]),
103
+ }),
104
+ ).rejects.toEqual(
105
+ new ConnectError(
106
+ 'operation actor_did is invalid DID',
107
+ Code.InvalidArgument,
108
+ ),
109
+ )
110
+ await expect(
111
+ client.putOperation({
112
+ collection: 'app.bsky.some.col',
113
+ actorDid: 'did:example:a',
114
+ rkey: '',
115
+ method: Method.CREATE,
116
+ payload: Buffer.from([]),
117
+ }),
118
+ ).rejects.toEqual(
119
+ new ConnectError('operation rkey is required', Code.InvalidArgument),
120
+ )
121
+ await expect(
122
+ client.putOperation({
123
+ collection: 'app.bsky.some.col',
124
+ actorDid: 'did:example:a',
125
+ rkey: 'rkey1',
126
+ method: Method.UNSPECIFIED,
127
+ payload: Buffer.from([]),
128
+ }),
129
+ ).rejects.toEqual(
130
+ new ConnectError('operation method is invalid', Code.InvalidArgument),
131
+ )
132
+ await expect(
133
+ client.putOperation({
134
+ collection: 'app.bsky.some.col',
135
+ actorDid: 'did:example:a',
136
+ rkey: 'rkey1',
137
+ method: Method.DELETE,
138
+ payload: Buffer.from([1, 2, 3]),
139
+ }),
140
+ ).rejects.toEqual(
141
+ new ConnectError(
142
+ 'cannot specify a payload when method is DELETE',
143
+ Code.InvalidArgument,
144
+ ),
145
+ )
146
+ })
147
+
148
+ it('puts operations.', async () => {
149
+ const res1 = await client.putOperation({
150
+ collection: 'app.bsky.some.col',
151
+ actorDid: 'did:example:a',
152
+ rkey: 'rkey1',
153
+ method: Method.CREATE,
154
+ payload: Buffer.from([1, 2, 3]),
155
+ })
156
+ const res2 = await client.putOperation({
157
+ collection: 'app.bsky.some.col',
158
+ actorDid: 'did:example:a',
159
+ rkey: 'rkey1',
160
+ method: Method.UPDATE,
161
+ payload: Buffer.from([4, 5, 6]),
162
+ })
163
+
164
+ expect(res1.operation?.id).toBe('1')
165
+ expect(res2.operation?.id).toBe('2')
166
+ expect(await dumpOps(bsync.ctx.db)).toStrictEqual([
167
+ {
168
+ id: 1,
169
+ collection: 'app.bsky.some.col',
170
+ actorDid: 'did:example:a',
171
+ rkey: 'rkey1',
172
+ method: Method.CREATE,
173
+ payload: Buffer.from([1, 2, 3]),
174
+ createdAt: expect.any(Date),
175
+ },
176
+ {
177
+ id: 2,
178
+ collection: 'app.bsky.some.col',
179
+ actorDid: 'did:example:a',
180
+ rkey: 'rkey1',
181
+ method: Method.UPDATE,
182
+ payload: Buffer.from([4, 5, 6]),
183
+ createdAt: expect.any(Date),
184
+ },
185
+ ])
186
+ })
187
+
188
+ it('returns the operations on creation.', async () => {
189
+ const res = await client.putOperation({
190
+ collection: 'app.bsky.some.col',
191
+ actorDid: 'did:example:a',
192
+ rkey: 'rkey1',
193
+ method: Method.CREATE,
194
+ payload: Buffer.from([1, 2, 3]),
195
+ })
196
+
197
+ const op = res.operation
198
+ assert(op)
199
+ // Compare each field individually to avoid custom serialization by proto response objects.
200
+ expect(op.id).toBe('3')
201
+ expect(op.collection).toBe('app.bsky.some.col')
202
+ expect(op.actorDid).toBe('did:example:a')
203
+ expect(op.rkey).toBe('rkey1')
204
+ expect(op.method).toBe(Method.CREATE)
205
+ expect(op.payload).toEqual(Uint8Array.from([1, 2, 3]))
206
+ })
207
+ })
208
+
209
+ describe('scanOperations', () => {
210
+ it('requires auth.', async () => {
211
+ // unauthed
212
+ const unauthedClient = createClient({
213
+ httpVersion: '1.1',
214
+ baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,
215
+ })
216
+ const tryScanOperations1 = unauthedClient.scanOperations({})
217
+ await expect(tryScanOperations1).rejects.toEqual(
218
+ new ConnectError('missing auth', Code.Unauthenticated),
219
+ )
220
+ // bad auth
221
+ const badauthedClient = createClient({
222
+ httpVersion: '1.1',
223
+ baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,
224
+ interceptors: [authWithApiKey('key-bad')],
225
+ })
226
+ const tryScanOperations2 = badauthedClient.scanOperations({})
227
+ await expect(tryScanOperations2).rejects.toEqual(
228
+ new ConnectError('invalid api key', Code.Unauthenticated),
229
+ )
230
+ })
231
+
232
+ it('pages over created ops.', async () => {
233
+ // add 100 ops
234
+ for (let i = 0; i < 100; ++i) {
235
+ await client.putOperation({
236
+ collection: 'app.bsky.some.col',
237
+ actorDid: `did:example:${i}`,
238
+ rkey: 'rkey1',
239
+ method: Method.CREATE,
240
+ payload: Buffer.from([1, 2, 3]),
241
+ })
242
+ }
243
+
244
+ let cursor: string | undefined
245
+ const operations: Operation[] = []
246
+ do {
247
+ const res = await client.scanOperations({
248
+ cursor,
249
+ limit: 30,
250
+ })
251
+ operations.push(...res.operations)
252
+ cursor = res.operations.length ? res.cursor : undefined
253
+ } while (cursor)
254
+
255
+ expect(operations.length).toEqual(100)
256
+ const operationIds = operations.map((op) => parseInt(op.id, 10))
257
+ const ascending = (a: number, b: number) => a - b
258
+ expect(operationIds).toEqual([...operationIds].sort(ascending))
259
+ })
260
+
261
+ it('supports long-poll, finding an operation.', async () => {
262
+ const scanPromise = client.scanOperations({})
263
+ await wait(100) // would be complete by now if it wasn't long-polling for an item
264
+ const { operation } = await client.putOperation({
265
+ collection: 'app.bsky.some.col',
266
+ actorDid: 'did:example:a',
267
+ rkey: 'rkey1',
268
+ method: Method.CREATE,
269
+ payload: Buffer.from([1, 2, 3]),
270
+ })
271
+ const res = await scanPromise
272
+ expect(res.operations.length).toEqual(1)
273
+ expect(res.operations[0]).toEqual(operation)
274
+ expect(res.cursor).toEqual(operation?.id)
275
+ })
276
+
277
+ it('supports long-poll, not finding an operation.', async () => {
278
+ const res = await client.scanOperations({})
279
+ expect(res.cursor).toEqual('')
280
+ expect(res.operations).toEqual([])
281
+ })
282
+ })
283
+ })
284
+
285
+ const dumpOps = async (db: Database) => {
286
+ return db.db
287
+ .selectFrom('operation')
288
+ .selectAll()
289
+ .orderBy('id', 'asc')
290
+ .execute()
291
+ }
292
+
293
+ const clearOps = async (db: Database) => {
294
+ await db.db.deleteFrom('operation').execute()
295
+ }
@@ -1 +1 @@
1
- {"root":["./src/client.ts","./src/config.ts","./src/context.ts","./src/index.ts","./src/logger.ts","./src/db/index.ts","./src/db/types.ts","./src/db/migrations/20240108T220751294Z-init.ts","./src/db/migrations/20240717T224303472Z-notif-ops.ts","./src/db/migrations/index.ts","./src/db/migrations/provider.ts","./src/db/schema/index.ts","./src/db/schema/mute_item.ts","./src/db/schema/mute_op.ts","./src/db/schema/notif_item.ts","./src/db/schema/notif_op.ts","./src/proto/bsync_connect.ts","./src/proto/bsync_pb.ts","./src/routes/add-mute-operation.ts","./src/routes/add-notif-operation.ts","./src/routes/auth.ts","./src/routes/index.ts","./src/routes/scan-mute-operations.ts","./src/routes/scan-notif-operations.ts","./src/routes/util.ts"],"version":"5.6.3"}
1
+ {"root":["./src/client.ts","./src/config.ts","./src/context.ts","./src/index.ts","./src/logger.ts","./src/db/index.ts","./src/db/types.ts","./src/db/migrations/20240108T220751294Z-init.ts","./src/db/migrations/20240717T224303472Z-notif-ops.ts","./src/db/migrations/20250527T022203400Z-add-operation.ts","./src/db/migrations/index.ts","./src/db/migrations/provider.ts","./src/db/schema/index.ts","./src/db/schema/mute_item.ts","./src/db/schema/mute_op.ts","./src/db/schema/notif_item.ts","./src/db/schema/notif_op.ts","./src/db/schema/operation.ts","./src/proto/bsync_connect.ts","./src/proto/bsync_pb.ts","./src/routes/add-mute-operation.ts","./src/routes/add-notif-operation.ts","./src/routes/auth.ts","./src/routes/index.ts","./src/routes/put-operation.ts","./src/routes/scan-mute-operations.ts","./src/routes/scan-notif-operations.ts","./src/routes/scan-operations.ts","./src/routes/util.ts"],"version":"5.8.2"}
@@ -1 +1 @@
1
- {"root":["./tests/mutes.test.ts","./tests/notifications.test.ts"],"version":"5.6.3"}
1
+ {"root":["./tests/mutes.test.ts","./tests/notifications.test.ts","./tests/operations.test.ts"],"version":"5.8.2"}