@atproto/ozone 0.1.49 → 0.1.51

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 (120) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/api/index.d.ts.map +1 -1
  3. package/dist/api/index.js +12 -0
  4. package/dist/api/index.js.map +1 -1
  5. package/dist/api/set/addValues.d.ts +4 -0
  6. package/dist/api/set/addValues.d.ts.map +1 -0
  7. package/dist/api/set/addValues.js +24 -0
  8. package/dist/api/set/addValues.js.map +1 -0
  9. package/dist/api/set/deleteSet.d.ts +4 -0
  10. package/dist/api/set/deleteSet.d.ts.map +1 -0
  11. package/dist/api/set/deleteSet.js +28 -0
  12. package/dist/api/set/deleteSet.js.map +1 -0
  13. package/dist/api/set/deleteValues.d.ts +4 -0
  14. package/dist/api/set/deleteValues.d.ts.map +1 -0
  15. package/dist/api/set/deleteValues.js +24 -0
  16. package/dist/api/set/deleteValues.js.map +1 -0
  17. package/dist/api/set/getValues.d.ts +4 -0
  18. package/dist/api/set/getValues.d.ts.map +1 -0
  19. package/dist/api/set/getValues.js +35 -0
  20. package/dist/api/set/getValues.js.map +1 -0
  21. package/dist/api/set/querySets.d.ts +4 -0
  22. package/dist/api/set/querySets.d.ts.map +1 -0
  23. package/dist/api/set/querySets.js +33 -0
  24. package/dist/api/set/querySets.js.map +1 -0
  25. package/dist/api/set/upsertSet.d.ts +4 -0
  26. package/dist/api/set/upsertSet.d.ts.map +1 -0
  27. package/dist/api/set/upsertSet.js +32 -0
  28. package/dist/api/set/upsertSet.js.map +1 -0
  29. package/dist/context.d.ts +3 -0
  30. package/dist/context.d.ts.map +1 -1
  31. package/dist/context.js +6 -0
  32. package/dist/context.js.map +1 -1
  33. package/dist/daemon/blob-diverter.d.ts +1 -1
  34. package/dist/daemon/blob-diverter.d.ts.map +1 -1
  35. package/dist/db/migrations/20241008T205730722Z-sets.d.ts +4 -0
  36. package/dist/db/migrations/20241008T205730722Z-sets.d.ts.map +1 -0
  37. package/dist/db/migrations/20241008T205730722Z-sets.js +46 -0
  38. package/dist/db/migrations/20241008T205730722Z-sets.js.map +1 -0
  39. package/dist/db/migrations/index.d.ts +1 -0
  40. package/dist/db/migrations/index.d.ts.map +1 -1
  41. package/dist/db/migrations/index.js +2 -1
  42. package/dist/db/migrations/index.js.map +1 -1
  43. package/dist/db/pagination.d.ts +1 -1
  44. package/dist/db/pagination.d.ts.map +1 -1
  45. package/dist/db/schema/index.d.ts +2 -1
  46. package/dist/db/schema/index.d.ts.map +1 -1
  47. package/dist/db/schema/ozone_set.d.ts +21 -0
  48. package/dist/db/schema/ozone_set.d.ts.map +1 -0
  49. package/dist/db/schema/ozone_set.js +6 -0
  50. package/dist/db/schema/ozone_set.js.map +1 -0
  51. package/dist/lexicon/index.d.ts +17 -0
  52. package/dist/lexicon/index.d.ts.map +1 -1
  53. package/dist/lexicon/index.js +44 -1
  54. package/dist/lexicon/index.js.map +1 -1
  55. package/dist/lexicon/lexicons.d.ts +292 -0
  56. package/dist/lexicon/lexicons.d.ts.map +1 -1
  57. package/dist/lexicon/lexicons.js +298 -0
  58. package/dist/lexicon/lexicons.js.map +1 -1
  59. package/dist/lexicon/types/tools/ozone/set/addValues.d.ts +32 -0
  60. package/dist/lexicon/types/tools/ozone/set/addValues.d.ts.map +1 -0
  61. package/dist/lexicon/types/tools/ozone/set/addValues.js +3 -0
  62. package/dist/lexicon/types/tools/ozone/set/addValues.js.map +1 -0
  63. package/dist/lexicon/types/tools/ozone/set/defs.d.ts +22 -0
  64. package/dist/lexicon/types/tools/ozone/set/defs.d.ts.map +1 -0
  65. package/dist/lexicon/types/tools/ozone/set/defs.js +24 -0
  66. package/dist/lexicon/types/tools/ozone/set/defs.js.map +1 -0
  67. package/dist/lexicon/types/tools/ozone/set/deleteSet.d.ts +41 -0
  68. package/dist/lexicon/types/tools/ozone/set/deleteSet.d.ts.map +1 -0
  69. package/dist/lexicon/types/tools/ozone/set/deleteSet.js +3 -0
  70. package/dist/lexicon/types/tools/ozone/set/deleteSet.js.map +1 -0
  71. package/dist/lexicon/types/tools/ozone/set/deleteValues.d.ts +33 -0
  72. package/dist/lexicon/types/tools/ozone/set/deleteValues.d.ts.map +1 -0
  73. package/dist/lexicon/types/tools/ozone/set/deleteValues.js +3 -0
  74. package/dist/lexicon/types/tools/ozone/set/deleteValues.js.map +1 -0
  75. package/dist/lexicon/types/tools/ozone/set/getValues.d.ts +41 -0
  76. package/dist/lexicon/types/tools/ozone/set/getValues.d.ts.map +1 -0
  77. package/dist/lexicon/types/tools/ozone/set/getValues.js +3 -0
  78. package/dist/lexicon/types/tools/ozone/set/getValues.js.map +1 -0
  79. package/dist/lexicon/types/tools/ozone/set/querySets.d.ts +42 -0
  80. package/dist/lexicon/types/tools/ozone/set/querySets.d.ts.map +1 -0
  81. package/dist/lexicon/types/tools/ozone/set/querySets.js +3 -0
  82. package/dist/lexicon/types/tools/ozone/set/querySets.js.map +1 -0
  83. package/dist/lexicon/types/tools/ozone/set/upsertSet.d.ts +35 -0
  84. package/dist/lexicon/types/tools/ozone/set/upsertSet.d.ts.map +1 -0
  85. package/dist/lexicon/types/tools/ozone/set/upsertSet.js +3 -0
  86. package/dist/lexicon/types/tools/ozone/set/upsertSet.js.map +1 -0
  87. package/dist/mod-service/subject.d.ts +9 -9
  88. package/dist/mod-service/subject.d.ts.map +1 -1
  89. package/dist/set/service.d.ts +46 -0
  90. package/dist/set/service.d.ts.map +1 -0
  91. package/dist/set/service.js +166 -0
  92. package/dist/set/service.js.map +1 -0
  93. package/package.json +3 -3
  94. package/src/api/index.ts +12 -0
  95. package/src/api/set/addValues.ts +28 -0
  96. package/src/api/set/deleteSet.ts +34 -0
  97. package/src/api/set/deleteValues.ts +31 -0
  98. package/src/api/set/getValues.ts +42 -0
  99. package/src/api/set/querySets.ts +36 -0
  100. package/src/api/set/upsertSet.ts +38 -0
  101. package/src/context.ts +8 -0
  102. package/src/daemon/blob-diverter.ts +1 -1
  103. package/src/db/migrations/20241008T205730722Z-sets.ts +53 -0
  104. package/src/db/migrations/index.ts +1 -0
  105. package/src/db/pagination.ts +1 -1
  106. package/src/db/schema/index.ts +2 -0
  107. package/src/db/schema/ozone_set.ts +24 -0
  108. package/src/lexicon/index.ts +82 -0
  109. package/src/lexicon/lexicons.ts +301 -0
  110. package/src/lexicon/types/tools/ozone/set/addValues.ts +41 -0
  111. package/src/lexicon/types/tools/ozone/set/defs.ts +44 -0
  112. package/src/lexicon/types/tools/ozone/set/deleteSet.ts +50 -0
  113. package/src/lexicon/types/tools/ozone/set/deleteValues.ts +42 -0
  114. package/src/lexicon/types/tools/ozone/set/getValues.ts +51 -0
  115. package/src/lexicon/types/tools/ozone/set/querySets.ts +52 -0
  116. package/src/lexicon/types/tools/ozone/set/upsertSet.ts +43 -0
  117. package/src/mod-service/subject.ts +9 -9
  118. package/src/set/service.ts +227 -0
  119. package/tests/__snapshots__/sets.test.ts.snap +46 -0
  120. package/tests/sets.test.ts +246 -0
@@ -0,0 +1,227 @@
1
+ import Database from '../db'
2
+ import { Selectable } from 'kysely'
3
+ import { SetDetail } from '../db/schema/ozone_set'
4
+ import { SetView } from '../lexicon/types/tools/ozone/set/defs'
5
+ import { paginate, TimeIdKeyset } from '../db/pagination'
6
+
7
+ export type SetServiceCreator = (db: Database) => SetService
8
+
9
+ export class SetService {
10
+ constructor(public db: Database) {}
11
+
12
+ static creator() {
13
+ return (db: Database) => new SetService(db)
14
+ }
15
+
16
+ buildQueryForSetWithSize() {
17
+ return this.db.db.selectFrom('set_detail as s').select([
18
+ 's.id',
19
+ 's.name',
20
+ 's.description',
21
+ 's.createdAt',
22
+ 's.updatedAt',
23
+ (eb) =>
24
+ eb
25
+ .selectFrom('set_value')
26
+ .select((e) => e.fn.count<number>('setId').as('count'))
27
+ .whereRef('setId', '=', 's.id')
28
+ .as('setSize'),
29
+ ])
30
+ }
31
+
32
+ async query({
33
+ limit,
34
+ cursor,
35
+ namePrefix,
36
+ sortBy,
37
+ sortDirection,
38
+ }: {
39
+ limit: number
40
+ cursor?: string
41
+ namePrefix?: string
42
+ sortBy: 'name' | 'createdAt' | 'updatedAt'
43
+ sortDirection: 'asc' | 'desc'
44
+ }): Promise<{
45
+ sets: Selectable<SetDetail & { setSize: number }>[]
46
+ cursor?: string
47
+ }> {
48
+ let qb = this.buildQueryForSetWithSize().limit(limit)
49
+
50
+ if (namePrefix) {
51
+ qb = qb.where('s.name', 'like', `${namePrefix}%`)
52
+ }
53
+
54
+ if (cursor) {
55
+ if (sortBy === 'name') {
56
+ qb = qb.where('s.name', sortDirection === 'asc' ? '>' : '<', cursor)
57
+ } else {
58
+ qb = qb.where(
59
+ `s.${sortBy}`,
60
+ sortDirection === 'asc' ? '>' : '<',
61
+ new Date(cursor),
62
+ )
63
+ }
64
+ }
65
+
66
+ qb = qb.orderBy(`s.${sortBy}`, sortDirection)
67
+
68
+ const sets = await qb.execute()
69
+ const lastItem = sets.at(-1)
70
+
71
+ return {
72
+ sets,
73
+ cursor: lastItem
74
+ ? sortBy === 'name'
75
+ ? lastItem?.name
76
+ : lastItem?.[sortBy].toISOString()
77
+ : undefined,
78
+ }
79
+ }
80
+
81
+ async getByName(name: string): Promise<Selectable<SetDetail> | undefined> {
82
+ const query = this.db.db
83
+ .selectFrom('set_detail')
84
+ .selectAll()
85
+ .where('name', '=', name)
86
+
87
+ return await query.executeTakeFirst()
88
+ }
89
+
90
+ async getByNameWithSize(
91
+ name: string,
92
+ ): Promise<Selectable<SetDetail & { setSize: number }> | undefined> {
93
+ return await this.buildQueryForSetWithSize()
94
+ .where('s.name', '=', name)
95
+ .executeTakeFirst()
96
+ }
97
+
98
+ async getSetWithValues({
99
+ name,
100
+ limit,
101
+ cursor,
102
+ }: {
103
+ name: string
104
+ limit: number
105
+ cursor?: string
106
+ }): Promise<
107
+ | {
108
+ set: Selectable<SetDetail & { setSize: number }>
109
+ values: string[]
110
+ cursor?: string
111
+ }
112
+ | undefined
113
+ > {
114
+ const set = await this.getByNameWithSize(name)
115
+ if (!set) return undefined
116
+
117
+ const { ref } = this.db.db.dynamic
118
+ const qb = this.db.db
119
+ .selectFrom('set_value')
120
+ .selectAll()
121
+ .where('setId', '=', set.id)
122
+
123
+ const keyset = new TimeIdKeyset(ref(`createdAt`), ref('id'))
124
+ const paginatedBuilder = paginate(qb, {
125
+ limit,
126
+ cursor,
127
+ keyset,
128
+ direction: 'asc',
129
+ })
130
+
131
+ const result = await paginatedBuilder.execute()
132
+
133
+ return {
134
+ set,
135
+ values: result.map((v) => v.value),
136
+ cursor: keyset.packFromResult(result),
137
+ }
138
+ }
139
+ async upsert({
140
+ name,
141
+ description,
142
+ }: Pick<SetDetail, 'name' | 'description'>): Promise<void> {
143
+ await this.db.db
144
+ .insertInto('set_detail')
145
+ .values({
146
+ name,
147
+ description,
148
+ updatedAt: new Date(),
149
+ })
150
+ .onConflict((oc) => {
151
+ // if description is provided as a string, even an empty one, update it
152
+ // otherwise, just update the updatedAt timestamp
153
+ return oc.column('name').doUpdateSet(
154
+ typeof description === 'string'
155
+ ? {
156
+ description,
157
+ updatedAt: new Date(),
158
+ }
159
+ : { updatedAt: new Date() },
160
+ )
161
+ })
162
+ .execute()
163
+ }
164
+
165
+ async addValues(setId: number, values: string[]): Promise<void> {
166
+ await this.db.transaction(async (txn) => {
167
+ const now = new Date()
168
+ const query = txn.db
169
+ .insertInto('set_value')
170
+ .values(
171
+ values.map((value) => ({
172
+ setId,
173
+ value,
174
+ createdAt: now,
175
+ })),
176
+ )
177
+ .onConflict((oc) => oc.columns(['setId', 'value']).doNothing())
178
+
179
+ await query.execute()
180
+
181
+ // Update the set's updatedAt timestamp
182
+ await txn.db
183
+ .updateTable('set_detail')
184
+ .set({ updatedAt: now })
185
+ .where('id', '=', setId)
186
+ .execute()
187
+ })
188
+ }
189
+
190
+ async removeValues(setId: number, values: string[]): Promise<void> {
191
+ if (values.length < 1) {
192
+ return
193
+ }
194
+ await this.db.transaction(async (txn) => {
195
+ const query = txn.db
196
+ .deleteFrom('set_value')
197
+ .where('setId', '=', setId)
198
+ .where('value', 'in', values)
199
+
200
+ await query.execute()
201
+
202
+ // Update the set's updatedAt timestamp
203
+ await txn.db
204
+ .updateTable('set_detail')
205
+ .set({ updatedAt: new Date() })
206
+ .where('id', '=', setId)
207
+ .execute()
208
+ })
209
+ }
210
+
211
+ async removeSet(setId: number): Promise<void> {
212
+ await this.db.transaction(async (txn) => {
213
+ await txn.db.deleteFrom('set_value').where('setId', '=', setId).execute()
214
+ await txn.db.deleteFrom('set_detail').where('id', '=', setId).execute()
215
+ })
216
+ }
217
+
218
+ view(set: Selectable<SetDetail> & { setSize: number }): SetView {
219
+ return {
220
+ name: set.name,
221
+ description: set.description || undefined,
222
+ setSize: set.setSize,
223
+ createdAt: set.createdAt.toISOString(),
224
+ updatedAt: set.updatedAt.toISOString(),
225
+ }
226
+ }
227
+ }
@@ -0,0 +1,46 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`ozone-sets querySets returns all sets when no parameters are provided 1`] = `
4
+ Array [
5
+ Object {
6
+ "createdAt": "1970-01-01T00:00:00.000Z",
7
+ "description": "Another test set",
8
+ "name": "another-set",
9
+ "setSize": 0,
10
+ "updatedAt": "1970-01-01T00:00:00.000Z",
11
+ },
12
+ Object {
13
+ "createdAt": "1970-01-01T00:00:00.000Z",
14
+ "description": "Test set 1",
15
+ "name": "test-set-1",
16
+ "setSize": 0,
17
+ "updatedAt": "1970-01-01T00:00:00.000Z",
18
+ },
19
+ Object {
20
+ "createdAt": "1970-01-01T00:00:00.000Z",
21
+ "name": "test-set-2",
22
+ "setSize": 0,
23
+ "updatedAt": "1970-01-01T00:00:00.000Z",
24
+ },
25
+ ]
26
+ `;
27
+
28
+ exports[`ozone-sets upsertSet creates a new set 1`] = `
29
+ Object {
30
+ "createdAt": "1970-01-01T00:00:00.000Z",
31
+ "description": "A new test set",
32
+ "name": "new-test-set",
33
+ "setSize": 0,
34
+ "updatedAt": "1970-01-01T00:00:00.000Z",
35
+ }
36
+ `;
37
+
38
+ exports[`ozone-sets upsertSet updates an existing set 1`] = `
39
+ Object {
40
+ "createdAt": "1970-01-01T00:00:00.000Z",
41
+ "description": "Updated description",
42
+ "name": "new-test-set",
43
+ "setSize": 0,
44
+ "updatedAt": "1970-01-01T00:00:00.000Z",
45
+ }
46
+ `;
@@ -0,0 +1,246 @@
1
+ import { TestNetwork, SeedClient, basicSeed } from '@atproto/dev-env'
2
+ import AtpAgent, {
3
+ ToolsOzoneSetDefs,
4
+ ToolsOzoneSetQuerySets,
5
+ } from '@atproto/api'
6
+ import { forSnapshot } from './_util'
7
+ import { ids } from '../src/lexicon/lexicons'
8
+
9
+ describe('ozone-sets', () => {
10
+ let network: TestNetwork
11
+ let agent: AtpAgent
12
+ let sc: SeedClient
13
+
14
+ const sampleSet1 = {
15
+ name: 'test-set-1',
16
+ description: 'Test set 1',
17
+ }
18
+
19
+ const sampleSet2 = {
20
+ name: 'test-set-2',
21
+ }
22
+
23
+ const sampleSet3 = {
24
+ name: 'another-set',
25
+ description: 'Another test set',
26
+ }
27
+
28
+ const upsertSet = async (set: ToolsOzoneSetDefs.Set) => {
29
+ const { data } = await agent.tools.ozone.set.upsertSet(set, {
30
+ encoding: 'application/json',
31
+ headers: await network.ozone.modHeaders(
32
+ ids.ToolsOzoneSetUpsertSet,
33
+ 'admin',
34
+ ),
35
+ })
36
+
37
+ return data
38
+ }
39
+
40
+ const removeSet = async (name: string) => {
41
+ await agent.tools.ozone.set.deleteSet(
42
+ { name },
43
+ {
44
+ encoding: 'application/json',
45
+ headers: await network.ozone.modHeaders(
46
+ ids.ToolsOzoneSetDeleteSet,
47
+ 'admin',
48
+ ),
49
+ },
50
+ )
51
+ }
52
+
53
+ const addValues = async (name: string, values: string[]) => {
54
+ await agent.tools.ozone.set.addValues(
55
+ { name, values },
56
+ {
57
+ encoding: 'application/json',
58
+ headers: await network.ozone.modHeaders(
59
+ ids.ToolsOzoneSetAddValues,
60
+ 'admin',
61
+ ),
62
+ },
63
+ )
64
+ }
65
+
66
+ const getValues = async (name: string, limit?: number, cursor?: string) => {
67
+ const { data } = await agent.tools.ozone.set.getValues(
68
+ { name, limit, cursor },
69
+ {
70
+ headers: await network.ozone.modHeaders(
71
+ ids.ToolsOzoneSetGetValues,
72
+ 'moderator',
73
+ ),
74
+ },
75
+ )
76
+ return data
77
+ }
78
+
79
+ const querySets = async (params: ToolsOzoneSetQuerySets.QueryParams) => {
80
+ const { data } = await agent.tools.ozone.set.querySets(params, {
81
+ headers: await network.ozone.modHeaders(
82
+ ids.ToolsOzoneSetQuerySets,
83
+ 'moderator',
84
+ ),
85
+ })
86
+ return data
87
+ }
88
+
89
+ beforeAll(async () => {
90
+ network = await TestNetwork.create({
91
+ dbPostgresSchema: 'ozone_sets',
92
+ })
93
+ agent = network.ozone.getClient()
94
+ sc = network.getSeedClient()
95
+ await basicSeed(sc)
96
+ await network.processAll()
97
+ })
98
+
99
+ afterAll(async () => {
100
+ await network.close()
101
+ })
102
+
103
+ describe('querySets', () => {
104
+ beforeAll(async () => {
105
+ await Promise.all([
106
+ upsertSet(sampleSet1),
107
+ upsertSet(sampleSet2),
108
+ upsertSet(sampleSet3),
109
+ ])
110
+ })
111
+ afterAll(async () => {
112
+ await Promise.all([
113
+ removeSet(sampleSet1.name),
114
+ removeSet(sampleSet2.name),
115
+ removeSet(sampleSet3.name),
116
+ ])
117
+ })
118
+ it('returns all sets when no parameters are provided', async () => {
119
+ const result = await querySets({})
120
+ expect(result.sets.length).toBe(3)
121
+ expect(forSnapshot(result.sets)).toMatchSnapshot()
122
+ })
123
+
124
+ it('limits the number of returned sets', async () => {
125
+ const result = await querySets({ limit: 2 })
126
+ expect(result.sets.length).toBe(2)
127
+ expect(result.cursor).toBeDefined()
128
+ })
129
+
130
+ it('returns sets after the cursor', async () => {
131
+ const firstPage = await querySets({ limit: 2 })
132
+ const secondPage = await querySets({ cursor: firstPage.cursor })
133
+ expect(secondPage.sets.length).toBe(1)
134
+ expect(secondPage.sets[0].name).toBe('test-set-2')
135
+ })
136
+
137
+ it('filters sets by name prefix', async () => {
138
+ const result = await querySets({ namePrefix: 'test-' })
139
+ expect(result.sets.length).toBe(2)
140
+ expect(result.sets.map((s) => s.name)).toEqual([
141
+ 'test-set-1',
142
+ 'test-set-2',
143
+ ])
144
+ })
145
+
146
+ it('sorts sets by given column and direction', async () => {
147
+ const sortedByName = await querySets({ sortBy: 'name' })
148
+ expect(sortedByName.sets.map((s) => s.name)).toEqual([
149
+ 'another-set',
150
+ 'test-set-1',
151
+ 'test-set-2',
152
+ ])
153
+ const reverseSortedByName = await querySets({
154
+ sortBy: 'name',
155
+ sortDirection: 'desc',
156
+ })
157
+ expect(reverseSortedByName.sets.map((s) => s.name)).toEqual([
158
+ 'test-set-2',
159
+ 'test-set-1',
160
+ 'another-set',
161
+ ])
162
+ })
163
+ })
164
+
165
+ describe('upsertSet', () => {
166
+ afterAll(async () => {
167
+ await removeSet('new-test-set')
168
+ })
169
+ it('creates a new set', async () => {
170
+ const result = await upsertSet({
171
+ name: 'new-test-set',
172
+ description: 'A new test set',
173
+ })
174
+ expect(forSnapshot(result)).toMatchSnapshot()
175
+ })
176
+
177
+ it('updates an existing set', async () => {
178
+ const result = await upsertSet({
179
+ name: 'new-test-set',
180
+ description: 'Updated description',
181
+ })
182
+ expect(forSnapshot(result)).toMatchSnapshot()
183
+ })
184
+
185
+ it('allows setting empty description', async () => {
186
+ const result = await upsertSet({
187
+ name: 'new-test-set',
188
+ description: '',
189
+ })
190
+ expect(result.description).toBeUndefined()
191
+ })
192
+ })
193
+
194
+ describe('addValues', () => {
195
+ beforeAll(async () => {
196
+ await upsertSet(sampleSet1)
197
+ await upsertSet(sampleSet2)
198
+ })
199
+ afterAll(async () => {
200
+ await removeSet(sampleSet1.name)
201
+ await removeSet(sampleSet2.name)
202
+ })
203
+ it('adds new values to an existing set', async () => {
204
+ const newValues = ['value1', 'value2', 'value3']
205
+ await addValues(sampleSet1.name, newValues)
206
+
207
+ const result = await getValues(sampleSet1.name)
208
+ expect(result.values).toEqual(expect.arrayContaining(newValues))
209
+ })
210
+
211
+ it('does not duplicate existing values', async () => {
212
+ const initialValues = ['initial1', 'initial2']
213
+ await addValues(sampleSet2.name, initialValues)
214
+
215
+ const newValues = ['initial2', 'new1', 'new2']
216
+ await addValues(sampleSet2.name, newValues)
217
+
218
+ const result = await getValues(sampleSet2.name)
219
+ expect(result.values).toEqual(
220
+ expect.arrayContaining([...initialValues, 'new1', 'new2']),
221
+ )
222
+ expect(result.values.filter((v) => v === 'initial2').length).toBe(1)
223
+ })
224
+ })
225
+
226
+ describe('getValues', () => {
227
+ beforeAll(async () => {
228
+ await upsertSet(sampleSet1)
229
+ })
230
+ afterAll(async () => {
231
+ await removeSet(sampleSet1.name)
232
+ })
233
+ it('paginates values from a set', async () => {
234
+ const allValues = Array.from({ length: 9 }, (_, i) => `value${i}`)
235
+ await addValues(sampleSet1.name, allValues)
236
+
237
+ const firstPage = await getValues(sampleSet1.name, 3)
238
+ const secondPage = await getValues(sampleSet1.name, 3, firstPage.cursor)
239
+ const lastPage = await getValues(sampleSet1.name, 3, secondPage.cursor)
240
+
241
+ expect(firstPage.values).toEqual(allValues.slice(0, 3))
242
+ expect(secondPage.values).toEqual(allValues.slice(3, 6))
243
+ expect(lastPage.values).toEqual(allValues.slice(6, 9))
244
+ })
245
+ })
246
+ })