@effect-app/infra 4.0.0-beta.81 → 4.0.0-beta.83
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 +20 -0
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +55 -32
- package/dist/Store/SQL/Pg.d.ts +4 -0
- package/dist/Store/SQL/Pg.d.ts.map +1 -0
- package/dist/Store/SQL/Pg.js +175 -0
- package/dist/Store/SQL/query.d.ts +34 -0
- package/dist/Store/SQL/query.d.ts.map +1 -0
- package/dist/Store/SQL/query.js +326 -0
- package/dist/Store/SQL.d.ts +4 -0
- package/dist/Store/SQL.d.ts.map +1 -0
- package/dist/Store/SQL.js +204 -0
- package/dist/Store/index.d.ts +1 -1
- package/dist/Store/index.d.ts.map +1 -1
- package/dist/Store/index.js +11 -1
- package/dist/Store/service.d.ts +2 -2
- package/dist/api/routing/middleware/middleware.d.ts +35 -1
- package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
- package/dist/api/routing/middleware/middleware.js +39 -1
- package/dist/api/setupRequest.d.ts +6 -3
- package/dist/api/setupRequest.d.ts.map +1 -1
- package/dist/api/setupRequest.js +11 -6
- package/package.json +19 -5
- package/src/Store/Cosmos.ts +200 -165
- package/src/Store/SQL/Pg.ts +296 -0
- package/src/Store/SQL/query.ts +372 -0
- package/src/Store/SQL.ts +332 -0
- package/src/Store/index.ts +10 -0
- package/src/Store/service.ts +2 -2
- package/src/api/routing/middleware/middleware.ts +43 -0
- package/src/api/setupRequest.ts +24 -4
- package/test/dist/sql-store.test.d.ts.map +1 -0
- package/test/sql-store.test.ts +553 -0
package/src/Store/Cosmos.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { InfraLogger } from "../logger.js"
|
|
|
9
9
|
import type { FieldValues } from "../Model/filter/types.js"
|
|
10
10
|
import { type RawQuery } from "../Model/query.js"
|
|
11
11
|
import { buildWhereCosmosQuery3, logQuery } from "./Cosmos/query.js"
|
|
12
|
+
import { storeId } from "./Memory.js"
|
|
12
13
|
import { type FilterArgs, type PersistenceModelType, type StorageConfig, type Store, type StoreConfig, StoreMaker } from "./service.js"
|
|
13
14
|
|
|
14
15
|
const makeMapId =
|
|
@@ -50,7 +51,21 @@ function makeCosmosStore({ prefix }: StorageConfig) {
|
|
|
50
51
|
}))
|
|
51
52
|
)
|
|
52
53
|
|
|
53
|
-
const
|
|
54
|
+
const basePartitionKey = config?.partitionValue() ?? "primary"
|
|
55
|
+
const nsPrefix = (ns: string) => ns === "primary" ? "" : `${ns}::`
|
|
56
|
+
const nsPartitionValue = (ns: string, e?: Encoded) => {
|
|
57
|
+
const base = config?.partitionValue(e) ?? "primary"
|
|
58
|
+
return `${nsPrefix(ns)}${base}`
|
|
59
|
+
}
|
|
60
|
+
const resolveNamespace = !config?.allowNamespace
|
|
61
|
+
? Effect.succeed("primary")
|
|
62
|
+
: storeId.asEffect().pipe(Effect.map((namespace) => {
|
|
63
|
+
if (namespace !== "primary" && !config.allowNamespace!(namespace)) {
|
|
64
|
+
throw new Error(`Namespace ${namespace} not allowed!`)
|
|
65
|
+
}
|
|
66
|
+
return namespace
|
|
67
|
+
}))
|
|
68
|
+
const resolvePartitionKey = Effect.map(resolveNamespace, (ns) => `${nsPrefix(ns)}${basePartitionKey}`)
|
|
54
69
|
|
|
55
70
|
const defaultValues = config?.defaultValues ?? {}
|
|
56
71
|
const container = db.container(containerId)
|
|
@@ -63,6 +78,7 @@ function makeCosmosStore({ prefix }: StorageConfig) {
|
|
|
63
78
|
const bulkSet = (items: NonEmptyReadonlyArray<PM>) =>
|
|
64
79
|
Effect
|
|
65
80
|
.gen(function*() {
|
|
81
|
+
const ns = yield* resolveNamespace
|
|
66
82
|
// TODO: disable batching if need atomicity
|
|
67
83
|
// we delay and batch to keep low amount of RUs
|
|
68
84
|
const b = [...items]
|
|
@@ -77,7 +93,7 @@ function makeCosmosStore({ prefix }: StorageConfig) {
|
|
|
77
93
|
resourceBody: {
|
|
78
94
|
...Struct.omit(x, ["_etag", idKey]),
|
|
79
95
|
id: x[idKey],
|
|
80
|
-
_partitionKey:
|
|
96
|
+
_partitionKey: nsPartitionValue(ns, x)
|
|
81
97
|
}
|
|
82
98
|
// don't use this or we get an error that the request and some item partition key dont match - makese no sense
|
|
83
99
|
// partitionKey: config?.partitionValue(x)
|
|
@@ -89,7 +105,7 @@ function makeCosmosStore({ prefix }: StorageConfig) {
|
|
|
89
105
|
resourceBody: {
|
|
90
106
|
...Struct.omit(x, ["_etag", idKey]),
|
|
91
107
|
id: x[idKey],
|
|
92
|
-
_partitionKey:
|
|
108
|
+
_partitionKey: nsPartitionValue(ns, x)
|
|
93
109
|
},
|
|
94
110
|
ifMatch: eTag
|
|
95
111
|
// don't use this or we get an error that the request and some item partition key dont match - makese no sense
|
|
@@ -166,65 +182,68 @@ function makeCosmosStore({ prefix }: StorageConfig) {
|
|
|
166
182
|
}, { captureStackTrace: false }))
|
|
167
183
|
|
|
168
184
|
const batchSet = (items: NonEmptyReadonlyArray<PM>) => {
|
|
169
|
-
return
|
|
170
|
-
.
|
|
171
|
-
|
|
172
|
-
(
|
|
173
|
-
[
|
|
174
|
-
x
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
185
|
+
return resolveNamespace
|
|
186
|
+
.pipe(Effect.flatMap((ns) =>
|
|
187
|
+
Effect
|
|
188
|
+
.suspend(() => {
|
|
189
|
+
const batch = [...items].map(
|
|
190
|
+
(x) =>
|
|
191
|
+
[
|
|
192
|
+
x,
|
|
193
|
+
Option.match(Option.fromNullishOr(x._etag), {
|
|
194
|
+
onNone: () => ({
|
|
195
|
+
operationType: "Create" as const,
|
|
196
|
+
resourceBody: {
|
|
197
|
+
...Struct.omit(x, ["_etag", idKey]),
|
|
198
|
+
id: x[idKey],
|
|
199
|
+
_partitionKey: nsPartitionValue(ns, x)
|
|
200
|
+
}
|
|
201
|
+
// don't use this or we get an error that the request and some item partition key dont match - makese no sense
|
|
202
|
+
// partitionKey: config?.partitionValue(x)
|
|
203
|
+
}),
|
|
204
|
+
onSome: (eTag) => ({
|
|
205
|
+
operationType: "Replace" as const,
|
|
206
|
+
id: x[idKey],
|
|
207
|
+
resourceBody: {
|
|
208
|
+
...Struct.omit(x, ["_etag", idKey]),
|
|
209
|
+
id: x[idKey],
|
|
210
|
+
_partitionKey: nsPartitionValue(ns, x)
|
|
211
|
+
},
|
|
212
|
+
// don't use this or we get an error that the request and some item partition key dont match - makese no sense
|
|
213
|
+
// partitionKey: config?.partitionValue(x)
|
|
214
|
+
ifMatch: eTag
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
] as const
|
|
218
|
+
)
|
|
201
219
|
|
|
202
|
-
|
|
220
|
+
const ex = batch.map(([, c]) => c)
|
|
203
221
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
return Effect
|
|
223
|
+
.promise(() => execBatch(ex, ex[0]?.resourceBody._partitionKey))
|
|
224
|
+
.pipe(Effect.flatMap(Effect.fnUntraced(function*(x) {
|
|
225
|
+
const result = x.result ?? []
|
|
226
|
+
const firstFailed = result.find(
|
|
227
|
+
(x: any) => x.statusCode > 299 || x.statusCode < 200
|
|
228
|
+
)
|
|
229
|
+
if (firstFailed) {
|
|
230
|
+
const code = firstFailed.statusCode ?? 0
|
|
231
|
+
if (code === 412 || code === 404 || code === 409) {
|
|
232
|
+
return yield* new OptimisticConcurrencyException({ type: name, id: "batch", code })
|
|
233
|
+
}
|
|
216
234
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
235
|
+
return yield* Effect.die(
|
|
236
|
+
new CosmosDbOperationError("not able to update record: " + code)
|
|
237
|
+
)
|
|
238
|
+
}
|
|
221
239
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
240
|
+
return batch.map(([e], i) => ({
|
|
241
|
+
...e,
|
|
242
|
+
_etag: result[i]?.eTag
|
|
243
|
+
})) as unknown as NonEmptyReadonlyArray<Encoded>
|
|
244
|
+
})))
|
|
245
|
+
})
|
|
246
|
+
))
|
|
228
247
|
.pipe(Effect
|
|
229
248
|
.withSpan("Cosmos.batchSet [effect-app/infra/Store]", {
|
|
230
249
|
attributes: { "repository.container_id": containerId, "repository.model_name": name }
|
|
@@ -234,14 +253,14 @@ function makeCosmosStore({ prefix }: StorageConfig) {
|
|
|
234
253
|
const s: Store<IdKey, Encoded> = {
|
|
235
254
|
queryRaw: <Out>(query: RawQuery<Encoded, Out>) =>
|
|
236
255
|
Effect
|
|
237
|
-
.sync(() => query.cosmos({ name }))
|
|
256
|
+
.all({ q: Effect.sync(() => query.cosmos({ name })), pk: resolvePartitionKey })
|
|
238
257
|
.pipe(
|
|
239
|
-
Effect.tap((q) => logQuery(q)),
|
|
240
|
-
Effect.flatMap((q) =>
|
|
258
|
+
Effect.tap(({ q }) => logQuery(q)),
|
|
259
|
+
Effect.flatMap(({ pk, q }) =>
|
|
241
260
|
Effect.promise(() =>
|
|
242
261
|
container
|
|
243
262
|
.items
|
|
244
|
-
.query<Out>(q, { partitionKey:
|
|
263
|
+
.query<Out>(q, { partitionKey: pk })
|
|
245
264
|
.fetchAll()
|
|
246
265
|
.then(({ resources }) =>
|
|
247
266
|
resources.map(
|
|
@@ -256,31 +275,36 @@ function makeCosmosStore({ prefix }: StorageConfig) {
|
|
|
256
275
|
}, { captureStackTrace: false })
|
|
257
276
|
),
|
|
258
277
|
batchRemove: (ids, partitionKey?: string) =>
|
|
259
|
-
Effect.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
278
|
+
resolvePartitionKey.pipe(Effect.flatMap((pk) =>
|
|
279
|
+
Effect.promise(() =>
|
|
280
|
+
execBatch(
|
|
281
|
+
mutable(ids.map((id) =>
|
|
282
|
+
dropUndefinedT({
|
|
283
|
+
operationType: "Delete" as const,
|
|
284
|
+
id
|
|
285
|
+
// don't use this or we get an error that the request and some item partition key dont match - makese no sense
|
|
286
|
+
// partitionKey: config?.partitionValue({ [idKey]: id } as Encoded)
|
|
287
|
+
})
|
|
288
|
+
)),
|
|
289
|
+
partitionKey ?? pk
|
|
290
|
+
)
|
|
270
291
|
)
|
|
271
|
-
),
|
|
292
|
+
)),
|
|
272
293
|
all: Effect
|
|
273
|
-
.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
294
|
+
.all({
|
|
295
|
+
q: Effect.sync(() => ({
|
|
296
|
+
query: `SELECT * FROM ${name}`,
|
|
297
|
+
parameters: []
|
|
298
|
+
})),
|
|
299
|
+
pk: resolvePartitionKey
|
|
300
|
+
})
|
|
277
301
|
.pipe(
|
|
278
|
-
Effect.tap((q) => logQuery(q)),
|
|
279
|
-
Effect.flatMap((q) =>
|
|
302
|
+
Effect.tap(({ q }) => logQuery(q)),
|
|
303
|
+
Effect.flatMap(({ pk, q }) =>
|
|
280
304
|
Effect.promise(() =>
|
|
281
305
|
container
|
|
282
306
|
.items
|
|
283
|
-
.query<PMCosmos>(q, { partitionKey:
|
|
307
|
+
.query<PMCosmos>(q, { partitionKey: pk })
|
|
284
308
|
.fetchAll()
|
|
285
309
|
.then(({ resources }) =>
|
|
286
310
|
resources.map(
|
|
@@ -305,27 +329,32 @@ function makeCosmosStore({ prefix }: StorageConfig) {
|
|
|
305
329
|
const filter = f.filter
|
|
306
330
|
type M = U extends undefined ? Encoded : Pick<Encoded, U>
|
|
307
331
|
return Effect
|
|
308
|
-
.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
332
|
+
.all({
|
|
333
|
+
q: Effect.sync(() =>
|
|
334
|
+
buildWhereCosmosQuery3(
|
|
335
|
+
idKey,
|
|
336
|
+
filter ? [{ t: "where-scope", result: filter, relation: "some" }] : [],
|
|
337
|
+
name,
|
|
338
|
+
defaultValues,
|
|
339
|
+
f.select as
|
|
340
|
+
| NonEmptyReadonlyArray<string | { key: string; subKeys: readonly string[] }>
|
|
341
|
+
| undefined,
|
|
342
|
+
f.order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
|
|
343
|
+
skip,
|
|
344
|
+
limit
|
|
345
|
+
)
|
|
346
|
+
),
|
|
347
|
+
pk: resolvePartitionKey
|
|
348
|
+
})
|
|
320
349
|
.pipe(
|
|
321
|
-
Effect.tap((q) => logQuery(q)),
|
|
350
|
+
Effect.tap(({ q }) => logQuery(q)),
|
|
322
351
|
Effect
|
|
323
|
-
.flatMap((q) =>
|
|
352
|
+
.flatMap(({ pk, q }) =>
|
|
324
353
|
Effect.promise(() =>
|
|
325
354
|
f.select
|
|
326
355
|
? container
|
|
327
356
|
.items
|
|
328
|
-
.query<M>(q, { partitionKey:
|
|
357
|
+
.query<M>(q, { partitionKey: pk })
|
|
329
358
|
.fetchAll()
|
|
330
359
|
.then(({ resources }) =>
|
|
331
360
|
resources.map((_) => ({
|
|
@@ -338,7 +367,7 @@ function makeCosmosStore({ prefix }: StorageConfig) {
|
|
|
338
367
|
)
|
|
339
368
|
: container
|
|
340
369
|
.items
|
|
341
|
-
.query<{ f: M }>(q, { partitionKey:
|
|
370
|
+
.query<{ f: M }>(q, { partitionKey: pk })
|
|
342
371
|
.fetchAll()
|
|
343
372
|
.then(({ resources }) =>
|
|
344
373
|
resources.map(({ f }) => ({ ...defaultValues, ...mapReverseId(f as any) }) as any)
|
|
@@ -353,80 +382,86 @@ function makeCosmosStore({ prefix }: StorageConfig) {
|
|
|
353
382
|
)
|
|
354
383
|
},
|
|
355
384
|
find: (id) =>
|
|
356
|
-
Effect
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
.pipe(Effect
|
|
366
|
-
.withSpan("Cosmos.find [effect-app/infra/Store]", {
|
|
367
|
-
attributes: {
|
|
368
|
-
"repository.container_id": containerId,
|
|
369
|
-
"repository.model_name": name,
|
|
370
|
-
partitionValue: config?.partitionValue({ [idKey]: id } as Encoded),
|
|
371
|
-
id
|
|
372
|
-
}
|
|
373
|
-
}, { captureStackTrace: false })),
|
|
374
|
-
set: (e) =>
|
|
375
|
-
Option
|
|
376
|
-
.match(
|
|
377
|
-
Option
|
|
378
|
-
.fromNullishOr(e._etag),
|
|
379
|
-
{
|
|
380
|
-
onNone: () =>
|
|
381
|
-
Effect.promise(() =>
|
|
382
|
-
container.items.create({
|
|
383
|
-
...mapId(e),
|
|
384
|
-
_partitionKey: config?.partitionValue(e)
|
|
385
|
-
})
|
|
386
|
-
),
|
|
387
|
-
onSome: (eTag) =>
|
|
388
|
-
Effect.promise(() =>
|
|
389
|
-
container.item(e[idKey], config?.partitionValue(e)).replace(
|
|
390
|
-
{ ...mapId(e), _partitionKey: config?.partitionValue(e) },
|
|
391
|
-
{
|
|
392
|
-
accessCondition: {
|
|
393
|
-
type: "IfMatch",
|
|
394
|
-
condition: eTag
|
|
395
|
-
}
|
|
396
|
-
}
|
|
385
|
+
resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
386
|
+
Effect
|
|
387
|
+
.promise(() =>
|
|
388
|
+
container
|
|
389
|
+
.item(id, nsPartitionValue(ns, { [idKey]: id } as Encoded))
|
|
390
|
+
.read<Encoded>()
|
|
391
|
+
.then(({ resource }) =>
|
|
392
|
+
Option.fromNullishOr(resource).pipe(
|
|
393
|
+
Option.map((_) => ({ ...defaultValues, ...mapReverseId(_) }))
|
|
397
394
|
)
|
|
398
395
|
)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
Effect
|
|
403
|
-
.flatMap((x) => {
|
|
404
|
-
if (x.statusCode === 412 || x.statusCode === 404 || x.statusCode === 409) {
|
|
405
|
-
return Effect.fail(
|
|
406
|
-
new OptimisticConcurrencyException({ type: name, id: e[idKey], code: x.statusCode })
|
|
407
|
-
)
|
|
408
|
-
}
|
|
409
|
-
if (x.statusCode > 299 || x.statusCode < 200) {
|
|
410
|
-
return Effect.die(
|
|
411
|
-
new CosmosDbOperationError(
|
|
412
|
-
"not able to update record: " + x.statusCode
|
|
413
|
-
)
|
|
414
|
-
)
|
|
415
|
-
}
|
|
416
|
-
return Effect.sync(() => ({
|
|
417
|
-
...e,
|
|
418
|
-
_etag: x.etag
|
|
419
|
-
}))
|
|
420
|
-
}),
|
|
421
|
-
Effect
|
|
422
|
-
.withSpan("Cosmos.set [effect-app/infra/Store]", {
|
|
396
|
+
)
|
|
397
|
+
.pipe(Effect
|
|
398
|
+
.withSpan("Cosmos.find [effect-app/infra/Store]", {
|
|
423
399
|
attributes: {
|
|
424
400
|
"repository.container_id": containerId,
|
|
425
401
|
"repository.model_name": name,
|
|
426
|
-
|
|
402
|
+
partitionValue: nsPartitionValue(ns, { [idKey]: id } as Encoded),
|
|
403
|
+
id
|
|
427
404
|
}
|
|
428
|
-
}, { captureStackTrace: false })
|
|
429
|
-
|
|
405
|
+
}, { captureStackTrace: false }))
|
|
406
|
+
)),
|
|
407
|
+
set: (e) =>
|
|
408
|
+
resolveNamespace.pipe(Effect.flatMap((ns) =>
|
|
409
|
+
Option
|
|
410
|
+
.match(
|
|
411
|
+
Option
|
|
412
|
+
.fromNullishOr(e._etag),
|
|
413
|
+
{
|
|
414
|
+
onNone: () =>
|
|
415
|
+
Effect.promise(() =>
|
|
416
|
+
container.items.create({
|
|
417
|
+
...mapId(e),
|
|
418
|
+
_partitionKey: nsPartitionValue(ns, e)
|
|
419
|
+
})
|
|
420
|
+
),
|
|
421
|
+
onSome: (eTag) =>
|
|
422
|
+
Effect.promise(() =>
|
|
423
|
+
container.item(e[idKey], nsPartitionValue(ns, e)).replace(
|
|
424
|
+
{ ...mapId(e), _partitionKey: nsPartitionValue(ns, e) },
|
|
425
|
+
{
|
|
426
|
+
accessCondition: {
|
|
427
|
+
type: "IfMatch",
|
|
428
|
+
condition: eTag
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
)
|
|
435
|
+
.pipe(
|
|
436
|
+
Effect
|
|
437
|
+
.flatMap((x) => {
|
|
438
|
+
if (x.statusCode === 412 || x.statusCode === 404 || x.statusCode === 409) {
|
|
439
|
+
return Effect.fail(
|
|
440
|
+
new OptimisticConcurrencyException({ type: name, id: e[idKey], code: x.statusCode })
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
if (x.statusCode > 299 || x.statusCode < 200) {
|
|
444
|
+
return Effect.die(
|
|
445
|
+
new CosmosDbOperationError(
|
|
446
|
+
"not able to update record: " + x.statusCode
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
return Effect.sync(() => ({
|
|
451
|
+
...e,
|
|
452
|
+
_etag: x.etag
|
|
453
|
+
}))
|
|
454
|
+
}),
|
|
455
|
+
Effect
|
|
456
|
+
.withSpan("Cosmos.set [effect-app/infra/Store]", {
|
|
457
|
+
attributes: {
|
|
458
|
+
"repository.container_id": containerId,
|
|
459
|
+
"repository.model_name": name,
|
|
460
|
+
id: e[idKey]
|
|
461
|
+
}
|
|
462
|
+
}, { captureStackTrace: false })
|
|
463
|
+
)
|
|
464
|
+
)),
|
|
430
465
|
batchSet,
|
|
431
466
|
bulkSet
|
|
432
467
|
}
|