@atproto/bsync 0.0.19 → 0.0.21
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.
- package/CHANGELOG.md +13 -0
- package/LICENSE.txt +1 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +1 -0
- package/dist/context.js.map +1 -1
- package/dist/db/migrations/20250527T022203400Z-add-operation.d.ts +4 -0
- package/dist/db/migrations/20250527T022203400Z-add-operation.d.ts.map +1 -0
- package/dist/db/migrations/20250527T022203400Z-add-operation.js +21 -0
- package/dist/db/migrations/20250527T022203400Z-add-operation.js.map +1 -0
- package/dist/db/migrations/20250603T163446567Z-alter-operation.d.ts +4 -0
- package/dist/db/migrations/20250603T163446567Z-alter-operation.d.ts.map +1 -0
- package/dist/db/migrations/20250603T163446567Z-alter-operation.js +19 -0
- package/dist/db/migrations/20250603T163446567Z-alter-operation.js.map +1 -0
- package/dist/db/migrations/index.d.ts +2 -0
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +3 -1
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/schema/index.d.ts +2 -1
- package/dist/db/schema/index.d.ts.map +1 -1
- package/dist/db/schema/operation.d.ts +18 -0
- package/dist/db/schema/operation.d.ts.map +1 -0
- package/dist/db/schema/operation.js +6 -0
- package/dist/db/schema/operation.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/proto/bsync_connect.d.ts +19 -1
- package/dist/proto/bsync_connect.d.ts.map +1 -1
- package/dist/proto/bsync_connect.js +18 -0
- package/dist/proto/bsync_connect.js.map +1 -1
- package/dist/proto/bsync_pb.d.ts +150 -0
- package/dist/proto/bsync_pb.d.ts.map +1 -1
- package/dist/proto/bsync_pb.js +401 -1
- package/dist/proto/bsync_pb.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +4 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/put-operation.d.ts +6 -0
- package/dist/routes/put-operation.d.ts.map +1 -0
- package/dist/routes/put-operation.js +91 -0
- package/dist/routes/put-operation.js.map +1 -0
- package/dist/routes/scan-operations.d.ts +6 -0
- package/dist/routes/scan-operations.d.ts.map +1 -0
- package/dist/routes/scan-operations.js +59 -0
- package/dist/routes/scan-operations.js.map +1 -0
- package/package.json +2 -2
- package/proto/bsync.proto +40 -0
- package/src/context.ts +2 -0
- package/src/db/migrations/20250527T022203400Z-add-operation.ts +20 -0
- package/src/db/migrations/20250603T163446567Z-alter-operation.ts +19 -0
- package/src/db/migrations/index.ts +2 -0
- package/src/db/schema/index.ts +3 -1
- package/src/db/schema/operation.ts +20 -0
- package/src/index.ts +5 -0
- package/src/proto/bsync_connect.ts +22 -0
- package/src/proto/bsync_pb.ts +355 -0
- package/src/routes/index.ts +4 -0
- package/src/routes/put-operation.ts +127 -0
- package/src/routes/scan-operations.ts +67 -0
- package/tests/operations.test.ts +327 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.tests.tsbuildinfo +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Code, ConnectError, ServiceImpl } from '@connectrpc/connect'
|
|
2
|
+
import { sql } from 'kysely'
|
|
3
|
+
import { ensureValidNsid, ensureValidRecordKey } from '@atproto/syntax'
|
|
4
|
+
import { AppContext } from '../context'
|
|
5
|
+
import { Database } from '../db'
|
|
6
|
+
import { OperationMethod, createOperationChannel } from '../db/schema/operation'
|
|
7
|
+
import { Service } from '../proto/bsync_connect'
|
|
8
|
+
import {
|
|
9
|
+
Method,
|
|
10
|
+
PutOperationRequest,
|
|
11
|
+
PutOperationResponse,
|
|
12
|
+
} from '../proto/bsync_pb'
|
|
13
|
+
import { authWithApiKey } from './auth'
|
|
14
|
+
import { isValidDid } from './util'
|
|
15
|
+
|
|
16
|
+
export default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({
|
|
17
|
+
async putOperation(req, handlerCtx) {
|
|
18
|
+
authWithApiKey(ctx, handlerCtx)
|
|
19
|
+
const { db } = ctx
|
|
20
|
+
const op = validateOp(req)
|
|
21
|
+
const id = await db.transaction(async (txn) => {
|
|
22
|
+
return putOp(txn, op)
|
|
23
|
+
})
|
|
24
|
+
return new PutOperationResponse({
|
|
25
|
+
operation: {
|
|
26
|
+
id: String(id),
|
|
27
|
+
actorDid: op.actorDid,
|
|
28
|
+
namespace: op.namespace,
|
|
29
|
+
key: op.key,
|
|
30
|
+
method: op.method,
|
|
31
|
+
payload: op.payload,
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const putOp = async (db: Database, op: Operation) => {
|
|
38
|
+
const { ref } = db.db.dynamic
|
|
39
|
+
const { id } = await db.db
|
|
40
|
+
.insertInto('operation')
|
|
41
|
+
.values({
|
|
42
|
+
actorDid: op.actorDid,
|
|
43
|
+
namespace: op.namespace,
|
|
44
|
+
key: op.key,
|
|
45
|
+
method: op.method,
|
|
46
|
+
payload: op.payload,
|
|
47
|
+
})
|
|
48
|
+
.returning('id')
|
|
49
|
+
.executeTakeFirstOrThrow()
|
|
50
|
+
await sql`notify ${ref(createOperationChannel)}`.execute(db.db) // emitted transactionally
|
|
51
|
+
return id
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const validateOp = (req: PutOperationRequest): Operation => {
|
|
55
|
+
try {
|
|
56
|
+
validateNamespace(req.namespace)
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new ConnectError(
|
|
59
|
+
'operation namespace is invalid NSID',
|
|
60
|
+
Code.InvalidArgument,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!isValidDid(req.actorDid)) {
|
|
65
|
+
throw new ConnectError(
|
|
66
|
+
'operation actor_did is invalid DID',
|
|
67
|
+
Code.InvalidArgument,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
ensureValidRecordKey(req.key)
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new ConnectError('operation key is required', Code.InvalidArgument)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
req.method !== Method.CREATE &&
|
|
79
|
+
req.method !== Method.UPDATE &&
|
|
80
|
+
req.method !== Method.DELETE
|
|
81
|
+
) {
|
|
82
|
+
throw new ConnectError('operation method is invalid', Code.InvalidArgument)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (req.method === Method.CREATE || req.method === Method.UPDATE) {
|
|
86
|
+
try {
|
|
87
|
+
JSON.parse(new TextDecoder().decode(req.payload))
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new ConnectError(
|
|
90
|
+
'payload must be a valid JSON when method is CREATE or UPDATE',
|
|
91
|
+
Code.InvalidArgument,
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (req.method === Method.DELETE && req.payload.length > 0) {
|
|
97
|
+
throw new ConnectError(
|
|
98
|
+
'cannot specify a payload when method is DELETE',
|
|
99
|
+
Code.InvalidArgument,
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return req as Operation
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const validateNamespace = (namespace: string): void => {
|
|
107
|
+
const parts = namespace.split('#')
|
|
108
|
+
|
|
109
|
+
if (parts.length !== 1 && parts.length !== 2) {
|
|
110
|
+
throw new Error('namespace must be in the format "nsid[#fragment]"')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const [nsid, fragment] = parts
|
|
114
|
+
|
|
115
|
+
ensureValidNsid(nsid)
|
|
116
|
+
if (fragment && !/^[a-zA-Z][a-zA-Z0-9]*$/.test(fragment)) {
|
|
117
|
+
throw new Error('namespace fragment must be a valid identifier')
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type Operation = {
|
|
122
|
+
actorDid: string
|
|
123
|
+
namespace: string
|
|
124
|
+
key: string
|
|
125
|
+
payload: Uint8Array
|
|
126
|
+
method: OperationMethod
|
|
127
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { once } from 'node:events'
|
|
2
|
+
import { ServiceImpl } from '@connectrpc/connect'
|
|
3
|
+
import { AppContext } from '../context'
|
|
4
|
+
import { createOperationChannel } from '../db/schema/operation'
|
|
5
|
+
import { Service } from '../proto/bsync_connect'
|
|
6
|
+
import { ScanOperationsResponse } from '../proto/bsync_pb'
|
|
7
|
+
import { authWithApiKey } from './auth'
|
|
8
|
+
import { combineSignals, validCursor } from './util'
|
|
9
|
+
|
|
10
|
+
export default (ctx: AppContext): Partial<ServiceImpl<typeof Service>> => ({
|
|
11
|
+
async scanOperations(req, handlerCtx) {
|
|
12
|
+
authWithApiKey(ctx, handlerCtx)
|
|
13
|
+
const { db, events } = ctx
|
|
14
|
+
const limit = req.limit || 1000
|
|
15
|
+
const cursor = validCursor(req.cursor)
|
|
16
|
+
const nextOpPromise = once(events, createOperationChannel, {
|
|
17
|
+
signal: combineSignals(
|
|
18
|
+
ctx.shutdown,
|
|
19
|
+
AbortSignal.timeout(ctx.cfg.service.longPollTimeoutMs),
|
|
20
|
+
),
|
|
21
|
+
})
|
|
22
|
+
nextOpPromise.catch(() => null) // ensure timeout is always handled
|
|
23
|
+
|
|
24
|
+
const nextOpPageQb = db.db
|
|
25
|
+
.selectFrom('operation')
|
|
26
|
+
.selectAll()
|
|
27
|
+
.where('id', '>', cursor ?? -1)
|
|
28
|
+
.orderBy('id', 'asc')
|
|
29
|
+
.limit(limit)
|
|
30
|
+
|
|
31
|
+
let ops = await nextOpPageQb.execute()
|
|
32
|
+
|
|
33
|
+
if (!ops.length) {
|
|
34
|
+
// if there were no ops on the page, wait for an event then try again.
|
|
35
|
+
try {
|
|
36
|
+
await nextOpPromise
|
|
37
|
+
} catch (err) {
|
|
38
|
+
ctx.shutdown.throwIfAborted()
|
|
39
|
+
return new ScanOperationsResponse({
|
|
40
|
+
operations: [],
|
|
41
|
+
cursor: req.cursor,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
ops = await nextOpPageQb.execute()
|
|
45
|
+
if (!ops.length) {
|
|
46
|
+
return new ScanOperationsResponse({
|
|
47
|
+
operations: [],
|
|
48
|
+
cursor: req.cursor,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const lastOp = ops[ops.length - 1]
|
|
54
|
+
|
|
55
|
+
return new ScanOperationsResponse({
|
|
56
|
+
operations: ops.map((op) => ({
|
|
57
|
+
id: op.id.toString(),
|
|
58
|
+
actorDid: op.actorDid,
|
|
59
|
+
namespace: op.namespace,
|
|
60
|
+
key: op.key,
|
|
61
|
+
method: op.method,
|
|
62
|
+
payload: op.payload,
|
|
63
|
+
})),
|
|
64
|
+
cursor: lastOp.id.toString(),
|
|
65
|
+
})
|
|
66
|
+
},
|
|
67
|
+
})
|
|
@@ -0,0 +1,327 @@
|
|
|
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
|
+
const validPayload0 = Buffer.from(JSON.stringify({ value: 0 }))
|
|
20
|
+
const validPayload1 = Buffer.from(JSON.stringify({ value: 1 }))
|
|
21
|
+
const invalidPayload = Buffer.from('{invalid json}')
|
|
22
|
+
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
bsync = await BsyncService.create(
|
|
25
|
+
envToCfg({
|
|
26
|
+
port: await getPort(),
|
|
27
|
+
dbUrl: process.env.DB_POSTGRES_URL,
|
|
28
|
+
dbSchema: 'bsync_operations',
|
|
29
|
+
apiKeys: ['key-1'],
|
|
30
|
+
longPollTimeoutMs: 500,
|
|
31
|
+
}),
|
|
32
|
+
)
|
|
33
|
+
await bsync.ctx.db.migrateToLatestOrThrow()
|
|
34
|
+
await bsync.start()
|
|
35
|
+
client = createClient({
|
|
36
|
+
httpVersion: '1.1',
|
|
37
|
+
baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,
|
|
38
|
+
interceptors: [authWithApiKey('key-1')],
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
afterAll(async () => {
|
|
43
|
+
await bsync.destroy()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
beforeEach(async () => {
|
|
47
|
+
await clearOps(bsync.ctx.db)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('putOperation', () => {
|
|
51
|
+
it('requires auth.', async () => {
|
|
52
|
+
// unauthed
|
|
53
|
+
const unauthedClient = createClient({
|
|
54
|
+
httpVersion: '1.1',
|
|
55
|
+
baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,
|
|
56
|
+
})
|
|
57
|
+
const tryPutOperation1 = unauthedClient.putOperation({
|
|
58
|
+
actorDid: 'did:example:a',
|
|
59
|
+
namespace: 'app.bsky.some.col',
|
|
60
|
+
key: 'key1',
|
|
61
|
+
method: Method.CREATE,
|
|
62
|
+
payload: validPayload0,
|
|
63
|
+
})
|
|
64
|
+
await expect(tryPutOperation1).rejects.toEqual(
|
|
65
|
+
new ConnectError('missing auth', Code.Unauthenticated),
|
|
66
|
+
)
|
|
67
|
+
// bad auth
|
|
68
|
+
const badauthedClient = createClient({
|
|
69
|
+
httpVersion: '1.1',
|
|
70
|
+
baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,
|
|
71
|
+
interceptors: [authWithApiKey('key-bad')],
|
|
72
|
+
})
|
|
73
|
+
const tryPutOperation2 = badauthedClient.putOperation({
|
|
74
|
+
actorDid: 'did:example:a',
|
|
75
|
+
namespace: 'app.bsky.some.col',
|
|
76
|
+
key: 'key1',
|
|
77
|
+
method: Method.CREATE,
|
|
78
|
+
payload: validPayload0,
|
|
79
|
+
})
|
|
80
|
+
await expect(tryPutOperation2).rejects.toEqual(
|
|
81
|
+
new ConnectError('invalid api key', Code.Unauthenticated),
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('fails on bad inputs.', async () => {
|
|
86
|
+
await expect(
|
|
87
|
+
client.putOperation({
|
|
88
|
+
actorDid: 'did:example:a',
|
|
89
|
+
namespace: 'bad-namespace',
|
|
90
|
+
key: 'key1',
|
|
91
|
+
method: Method.CREATE,
|
|
92
|
+
payload: validPayload0,
|
|
93
|
+
}),
|
|
94
|
+
).rejects.toEqual(
|
|
95
|
+
new ConnectError(
|
|
96
|
+
'operation namespace is invalid NSID',
|
|
97
|
+
Code.InvalidArgument,
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
await expect(
|
|
101
|
+
client.putOperation({
|
|
102
|
+
actorDid: 'bad-did',
|
|
103
|
+
namespace: 'app.bsky.some.col',
|
|
104
|
+
key: 'key1',
|
|
105
|
+
method: Method.CREATE,
|
|
106
|
+
payload: validPayload0,
|
|
107
|
+
}),
|
|
108
|
+
).rejects.toEqual(
|
|
109
|
+
new ConnectError(
|
|
110
|
+
'operation actor_did is invalid DID',
|
|
111
|
+
Code.InvalidArgument,
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
await expect(
|
|
115
|
+
client.putOperation({
|
|
116
|
+
actorDid: 'did:example:a',
|
|
117
|
+
namespace: 'app.bsky.some.col',
|
|
118
|
+
key: '',
|
|
119
|
+
method: Method.CREATE,
|
|
120
|
+
payload: validPayload0,
|
|
121
|
+
}),
|
|
122
|
+
).rejects.toEqual(
|
|
123
|
+
new ConnectError('operation key is required', Code.InvalidArgument),
|
|
124
|
+
)
|
|
125
|
+
await expect(
|
|
126
|
+
client.putOperation({
|
|
127
|
+
actorDid: 'did:example:a',
|
|
128
|
+
namespace: 'app.bsky.some.col',
|
|
129
|
+
key: 'key1',
|
|
130
|
+
method: Method.UNSPECIFIED,
|
|
131
|
+
payload: validPayload0,
|
|
132
|
+
}),
|
|
133
|
+
).rejects.toEqual(
|
|
134
|
+
new ConnectError('operation method is invalid', Code.InvalidArgument),
|
|
135
|
+
)
|
|
136
|
+
await expect(
|
|
137
|
+
client.putOperation({
|
|
138
|
+
actorDid: 'did:example:a',
|
|
139
|
+
namespace: 'app.bsky.some.col',
|
|
140
|
+
key: 'key1',
|
|
141
|
+
method: Method.CREATE,
|
|
142
|
+
payload: invalidPayload,
|
|
143
|
+
}),
|
|
144
|
+
).rejects.toEqual(
|
|
145
|
+
new ConnectError(
|
|
146
|
+
'payload must be a valid JSON when method is CREATE or UPDATE',
|
|
147
|
+
Code.InvalidArgument,
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
await expect(
|
|
151
|
+
client.putOperation({
|
|
152
|
+
actorDid: 'did:example:a',
|
|
153
|
+
namespace: 'app.bsky.some.col',
|
|
154
|
+
key: 'key1',
|
|
155
|
+
method: Method.UPDATE,
|
|
156
|
+
payload: invalidPayload,
|
|
157
|
+
}),
|
|
158
|
+
).rejects.toEqual(
|
|
159
|
+
new ConnectError(
|
|
160
|
+
'payload must be a valid JSON when method is CREATE or UPDATE',
|
|
161
|
+
Code.InvalidArgument,
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
await expect(
|
|
165
|
+
client.putOperation({
|
|
166
|
+
actorDid: 'did:example:a',
|
|
167
|
+
namespace: 'app.bsky.some.col',
|
|
168
|
+
key: 'key1',
|
|
169
|
+
method: Method.DELETE,
|
|
170
|
+
payload: validPayload0,
|
|
171
|
+
}),
|
|
172
|
+
).rejects.toEqual(
|
|
173
|
+
new ConnectError(
|
|
174
|
+
'cannot specify a payload when method is DELETE',
|
|
175
|
+
Code.InvalidArgument,
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('puts operations.', async () => {
|
|
181
|
+
const res1 = await client.putOperation({
|
|
182
|
+
actorDid: 'did:example:a',
|
|
183
|
+
namespace: 'app.bsky.some.col',
|
|
184
|
+
key: 'key1',
|
|
185
|
+
method: Method.CREATE,
|
|
186
|
+
payload: validPayload0,
|
|
187
|
+
})
|
|
188
|
+
const res2 = await client.putOperation({
|
|
189
|
+
actorDid: 'did:example:a',
|
|
190
|
+
namespace: 'app.bsky.other.col#id',
|
|
191
|
+
key: 'key1',
|
|
192
|
+
method: Method.UPDATE,
|
|
193
|
+
payload: validPayload1,
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
expect(res1.operation?.id).toBe('1')
|
|
197
|
+
expect(res2.operation?.id).toBe('2')
|
|
198
|
+
expect(await dumpOps(bsync.ctx.db)).toStrictEqual([
|
|
199
|
+
{
|
|
200
|
+
id: 1,
|
|
201
|
+
actorDid: 'did:example:a',
|
|
202
|
+
namespace: 'app.bsky.some.col',
|
|
203
|
+
key: 'key1',
|
|
204
|
+
method: Method.CREATE,
|
|
205
|
+
payload: validPayload0,
|
|
206
|
+
createdAt: expect.any(Date),
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 2,
|
|
210
|
+
actorDid: 'did:example:a',
|
|
211
|
+
namespace: 'app.bsky.other.col#id',
|
|
212
|
+
key: 'key1',
|
|
213
|
+
method: Method.UPDATE,
|
|
214
|
+
payload: validPayload1,
|
|
215
|
+
createdAt: expect.any(Date),
|
|
216
|
+
},
|
|
217
|
+
])
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('returns the operations on creation.', async () => {
|
|
221
|
+
const res = await client.putOperation({
|
|
222
|
+
actorDid: 'did:example:a',
|
|
223
|
+
namespace: 'app.bsky.some.col',
|
|
224
|
+
key: 'key1',
|
|
225
|
+
method: Method.CREATE,
|
|
226
|
+
payload: validPayload0,
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const op = res.operation
|
|
230
|
+
assert(op)
|
|
231
|
+
// Compare each field individually to avoid custom serialization by proto response objects.
|
|
232
|
+
expect(op.id).toBe('3')
|
|
233
|
+
expect(op.actorDid).toBe('did:example:a')
|
|
234
|
+
expect(op.namespace).toBe('app.bsky.some.col')
|
|
235
|
+
expect(op.key).toBe('key1')
|
|
236
|
+
expect(op.method).toBe(Method.CREATE)
|
|
237
|
+
expect(op.payload).toEqual(new Uint8Array(validPayload0))
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
describe('scanOperations', () => {
|
|
242
|
+
it('requires auth.', async () => {
|
|
243
|
+
// unauthed
|
|
244
|
+
const unauthedClient = createClient({
|
|
245
|
+
httpVersion: '1.1',
|
|
246
|
+
baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,
|
|
247
|
+
})
|
|
248
|
+
const tryScanOperations1 = unauthedClient.scanOperations({})
|
|
249
|
+
await expect(tryScanOperations1).rejects.toEqual(
|
|
250
|
+
new ConnectError('missing auth', Code.Unauthenticated),
|
|
251
|
+
)
|
|
252
|
+
// bad auth
|
|
253
|
+
const badauthedClient = createClient({
|
|
254
|
+
httpVersion: '1.1',
|
|
255
|
+
baseUrl: `http://localhost:${bsync.ctx.cfg.service.port}`,
|
|
256
|
+
interceptors: [authWithApiKey('key-bad')],
|
|
257
|
+
})
|
|
258
|
+
const tryScanOperations2 = badauthedClient.scanOperations({})
|
|
259
|
+
await expect(tryScanOperations2).rejects.toEqual(
|
|
260
|
+
new ConnectError('invalid api key', Code.Unauthenticated),
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('pages over created ops.', async () => {
|
|
265
|
+
// add 100 ops
|
|
266
|
+
for (let i = 0; i < 100; ++i) {
|
|
267
|
+
await client.putOperation({
|
|
268
|
+
actorDid: `did:example:${i}`,
|
|
269
|
+
namespace: 'app.bsky.some.col',
|
|
270
|
+
key: 'key1',
|
|
271
|
+
method: Method.CREATE,
|
|
272
|
+
payload: validPayload0,
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let cursor: string | undefined
|
|
277
|
+
const operations: Operation[] = []
|
|
278
|
+
do {
|
|
279
|
+
const res = await client.scanOperations({
|
|
280
|
+
cursor,
|
|
281
|
+
limit: 30,
|
|
282
|
+
})
|
|
283
|
+
operations.push(...res.operations)
|
|
284
|
+
cursor = res.operations.length ? res.cursor : undefined
|
|
285
|
+
} while (cursor)
|
|
286
|
+
|
|
287
|
+
expect(operations.length).toEqual(100)
|
|
288
|
+
const operationIds = operations.map((op) => parseInt(op.id, 10))
|
|
289
|
+
const ascending = (a: number, b: number) => a - b
|
|
290
|
+
expect(operationIds).toEqual([...operationIds].sort(ascending))
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('supports long-poll, finding an operation.', async () => {
|
|
294
|
+
const scanPromise = client.scanOperations({})
|
|
295
|
+
await wait(100) // would be complete by now if it wasn't long-polling for an item
|
|
296
|
+
const { operation } = await client.putOperation({
|
|
297
|
+
actorDid: 'did:example:a',
|
|
298
|
+
namespace: 'app.bsky.some.col',
|
|
299
|
+
key: 'key1',
|
|
300
|
+
method: Method.CREATE,
|
|
301
|
+
payload: validPayload0,
|
|
302
|
+
})
|
|
303
|
+
const res = await scanPromise
|
|
304
|
+
expect(res.operations.length).toEqual(1)
|
|
305
|
+
expect(res.operations[0]).toEqual(operation)
|
|
306
|
+
expect(res.cursor).toEqual(operation?.id)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('supports long-poll, not finding an operation.', async () => {
|
|
310
|
+
const res = await client.scanOperations({})
|
|
311
|
+
expect(res.cursor).toEqual('')
|
|
312
|
+
expect(res.operations).toEqual([])
|
|
313
|
+
})
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const dumpOps = async (db: Database) => {
|
|
318
|
+
return db.db
|
|
319
|
+
.selectFrom('operation')
|
|
320
|
+
.selectAll()
|
|
321
|
+
.orderBy('id', 'asc')
|
|
322
|
+
.execute()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const clearOps = async (db: Database) => {
|
|
326
|
+
await db.db.deleteFrom('operation').execute()
|
|
327
|
+
}
|
|
@@ -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.8.2"}
|
|
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/20250603T163446567Z-alter-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.8.
|
|
1
|
+
{"root":["./tests/mutes.test.ts","./tests/notifications.test.ts","./tests/operations.test.ts"],"version":"5.8.3"}
|