@lucashw68/nsdb 1.0.0-rc.2

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 (49) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/GET_STARTED.md +709 -0
  3. package/LICENSE +21 -0
  4. package/README.md +159 -0
  5. package/cli/index.js +83 -0
  6. package/helpers/args.js +22 -0
  7. package/helpers/config.js +142 -0
  8. package/helpers/generated.js +48 -0
  9. package/helpers/io.js +39 -0
  10. package/helpers/metadata.js +19 -0
  11. package/helpers/names.js +16 -0
  12. package/helpers/relations.js +101 -0
  13. package/helpers/shell.js +15 -0
  14. package/helpers/tables.js +79 -0
  15. package/helpers/ts.js +37 -0
  16. package/module.ts +151 -0
  17. package/nsdb.config.example.mjs +39 -0
  18. package/nsdb.config.example.ts +42 -0
  19. package/package.json +114 -0
  20. package/runtime/components/Form/NsdbRelationSelect.vue +258 -0
  21. package/runtime/components/NsdbForm.vue +865 -0
  22. package/runtime/components/NsdbList.vue +961 -0
  23. package/runtime/composables/useNsdbProfile.ts +119 -0
  24. package/runtime/composables/useNsdbSchemas.ts +176 -0
  25. package/runtime/composables/useSupabaseApi.ts +177 -0
  26. package/runtime/composables/useSupabaseApiStorage.ts +337 -0
  27. package/runtime/composables/useSupabaseModels.ts +412 -0
  28. package/runtime/query.ts +126 -0
  29. package/runtime/stores/createDbStore.ts +439 -0
  30. package/runtime/stores/createSingletonDbStore.ts +67 -0
  31. package/runtime/utils/dataFreshness.ts +47 -0
  32. package/runtime/utils/storage.ts +41 -0
  33. package/scripts/clear.js +64 -0
  34. package/scripts/generate-composables.js +100 -0
  35. package/scripts/generate-enums.js +106 -0
  36. package/scripts/generate-metadata.js +165 -0
  37. package/scripts/generate-models.js +164 -0
  38. package/scripts/generate-schemas.js +443 -0
  39. package/scripts/generate-stores.js +90 -0
  40. package/scripts/generate-types.js +196 -0
  41. package/scripts/init.js +225 -0
  42. package/templates/model.template.ts +48 -0
  43. package/templates/schema.template.ts +13 -0
  44. package/templates/useNsdbModel.template.ts +9 -0
  45. package/types/config.ts +50 -0
  46. package/types/entities.ts +66 -0
  47. package/types/index.ts +14 -0
  48. package/types/list.ts +78 -0
  49. package/types/model.ts +57 -0
@@ -0,0 +1,412 @@
1
+ import { useSupabaseApi } from './useSupabaseApi'
2
+ import { useSupabaseClient, useSupabaseUser } from '#imports'
3
+ import type { ModelHandle, ModelMutationTarget, ModelQuery } from '@lucashw68/nsdb/types/model'
4
+ import type { OrderDirection } from '@lucashw68/nsdb/types/list'
5
+ import type { Ref } from 'vue'
6
+ import { computed, isRef, ref, watch } from 'vue'
7
+ import { isComplexCollectionQuery, sortCollection } from '../utils/dataFreshness'
8
+
9
+ type MutationPayload = Record<string, unknown> | Record<string, unknown>[]
10
+
11
+ export interface StoreLike<T, TInsert = Partial<T>, TUpdate = Partial<T>> {
12
+ items: Ref<T[]> | T[]
13
+ totalCount?: Ref<number | null> | number | null
14
+ loading?: Ref<boolean> | boolean
15
+ error?: Ref<unknown> | unknown
16
+ stale?: Ref<boolean> | boolean
17
+ getById: (id: string | number) => T | null
18
+ create: (payload: TInsert) => Promise<T | null>
19
+ update: (id: string | number, payload: TUpdate) => Promise<T | null>
20
+ remove: (id: string | number) => void | Promise<void>
21
+ fetchFromSupabase: (query?: any) => Promise<T[]>
22
+ subscribe?: () => void
23
+ unsubscribe?: () => void | Promise<void>
24
+ refresh?: (query?: any) => Promise<T[]>
25
+ invalidate?: () => void
26
+ }
27
+
28
+ type Options<T, TInsert, TUpdate, TPrimaryKey extends Extract<keyof T, string>> =
29
+ | boolean
30
+ | { store?: boolean; storeCreator?: () => StoreLike<T, TInsert, TUpdate>; primaryKey?: TPrimaryKey }
31
+
32
+ export type { ModelHandle, ModelQuery } from '@lucashw68/nsdb/types/model'
33
+
34
+ /**
35
+ * Résultat normalisé pour l'order.
36
+ */
37
+ type NormalizedOrder = {
38
+ orderBy: string
39
+ orderDirection: OrderDirection
40
+ orderForeignTable?: string
41
+ }
42
+
43
+ function normalizeStoreItems<T>(store: { items: Ref<T[]> | T[] }): Ref<T[]> {
44
+ if (isRef(store.items)) return store.items
45
+
46
+ return computed({
47
+ get: () => store.items as T[],
48
+ set: (value) => {
49
+ ;(store as any).items = value
50
+ },
51
+ })
52
+ }
53
+
54
+ function normalizeStoreTotalCount(store: { totalCount?: Ref<number | null> | number | null }): Ref<number | null> {
55
+ if (isRef(store.totalCount)) return store.totalCount
56
+
57
+ return computed({
58
+ get: () => typeof store.totalCount === 'number' ? store.totalCount : null,
59
+ set: (value) => {
60
+ ;(store as any).totalCount = value
61
+ },
62
+ })
63
+ }
64
+
65
+ function normalizeStoreRef<T>(store: Record<string, any>, key: string, fallback: T): Ref<T> {
66
+ if (isRef(store[key])) return store[key] as Ref<T>
67
+ return computed({
68
+ get: () => (store[key] ?? fallback) as T,
69
+ set: value => { store[key] = value },
70
+ })
71
+ }
72
+
73
+ function parseOrderPath(path: string): { foreignTable?: string; column: string } {
74
+ if (!path.includes('.')) return { column: path }
75
+
76
+ const parts = path.split('.').filter(Boolean)
77
+ if (parts.length === 2) {
78
+ const foreignTable = parts[0]
79
+ const column = parts[1]
80
+ if (!foreignTable || !column) return { column: path }
81
+ return { foreignTable, column }
82
+ }
83
+
84
+ // cas non supporté proprement (ex: a.b.c)
85
+ return { column: path }
86
+ }
87
+
88
+ function normalizeOrder(
89
+ rawOrderBy: ModelQuery<string, string>['orderBy'],
90
+ rawOrderDirection?: OrderDirection,
91
+ rawOrderForeignTable?: string,
92
+ defaultOrderBy: string = 'id',
93
+ ): NormalizedOrder {
94
+ let orderBy = defaultOrderBy
95
+ let orderDirection: OrderDirection = rawOrderDirection ?? 'asc'
96
+ let orderForeignTable: string | undefined = rawOrderForeignTable
97
+
98
+ if (typeof rawOrderBy === 'string') {
99
+ const parsed = parseOrderPath(rawOrderBy)
100
+ orderBy = parsed.column
101
+ orderForeignTable = parsed.foreignTable ?? rawOrderForeignTable
102
+ return { orderBy, orderDirection, orderForeignTable }
103
+ }
104
+
105
+ if (rawOrderBy && typeof rawOrderBy === 'object') {
106
+ const [rawColumn, direction] = Object.entries(rawOrderBy)[0] as [
107
+ string,
108
+ OrderDirection | undefined
109
+ ]
110
+ const parsed = parseOrderPath(rawColumn)
111
+
112
+ orderBy = parsed.column
113
+ orderForeignTable = parsed.foreignTable ?? rawOrderForeignTable
114
+ orderDirection = direction ?? rawOrderDirection ?? 'asc'
115
+
116
+ return { orderBy, orderDirection, orderForeignTable }
117
+ }
118
+
119
+ return { orderBy, orderDirection, orderForeignTable }
120
+ }
121
+
122
+ /**
123
+ * Unified CRUD abstraction over a Supabase table.
124
+ */
125
+ export function useSupabaseModel<
126
+ TRow,
127
+ TInsert = Partial<TRow>,
128
+ TUpdate = Partial<TRow>,
129
+ TPrimaryKey extends Extract<keyof TRow, string> = Extract<keyof TRow, string>,
130
+ >(
131
+ modelName: string,
132
+ opts: Options<TRow, TInsert, TUpdate, TPrimaryKey> = false
133
+ ): ModelHandle<TRow, TInsert, TUpdate, TPrimaryKey> {
134
+ const useStore = typeof opts === 'boolean' ? opts : !!opts.store
135
+ const storeCreator =
136
+ typeof opts === 'object' && opts.store ? opts.storeCreator : undefined
137
+ const primaryKey = typeof opts === 'object' ? opts.primaryKey ?? 'id' : 'id'
138
+ type RowQuery = ModelQuery<string, Extract<keyof TRow, string>>
139
+ type MutationTarget = ModelMutationTarget<TRow, TPrimaryKey>
140
+
141
+ const resolveMutationTarget = (target: MutationTarget, operation: 'update' | 'remove'): string | number => {
142
+ const value = typeof target === 'object' && target !== null
143
+ ? (target as Record<string, unknown>)[primaryKey]
144
+ : target
145
+
146
+ if (value === null || value === undefined) {
147
+ throw new Error(`Cannot ${operation} "${modelName}": target is missing primary key "${primaryKey}".`)
148
+ }
149
+
150
+ return value as string | number
151
+ }
152
+
153
+ // ############################################################
154
+ // # STORE MODE (Pinia / offline)
155
+ // ############################################################
156
+ if (useStore) {
157
+ if (!storeCreator) {
158
+ throw new Error(
159
+ `❌ useSupabaseModel("${modelName}", { store: true }) requires a storeCreator`
160
+ )
161
+ }
162
+
163
+ const store = storeCreator()
164
+ const noOp = () => {}
165
+ const storeItems = normalizeStoreItems(store)
166
+ const totalCount = normalizeStoreTotalCount(store)
167
+ const loading = normalizeStoreRef<boolean>(store as any, 'loading', false)
168
+ const error = normalizeStoreRef<unknown>(store as any, 'error', null)
169
+ const stale = normalizeStoreRef<boolean>(store as any, 'stale', true)
170
+
171
+ const getById = async (id: string | number) =>
172
+ (store.getById(id) as TRow | null) ?? null
173
+ const create = async (payload: TInsert): Promise<TRow> => {
174
+ const created = await store.create(payload)
175
+ if (created === null || created === undefined) {
176
+ throw new Error(`Cannot create "${modelName}": Supabase returned no row.`)
177
+ }
178
+ return created
179
+ }
180
+ const update = (target: MutationTarget, payload: TUpdate) =>
181
+ store.update(resolveMutationTarget(target, 'update'), payload)
182
+ const remove = async (target: MutationTarget) => {
183
+ await store.remove(resolveMutationTarget(target, 'remove'))
184
+ }
185
+ const fetch = async (query?: RowQuery) =>
186
+ (await store.fetchFromSupabase(query)) as TRow[]
187
+ const refresh = async (query?: RowQuery) => store.refresh
188
+ ? await store.refresh(query) as TRow[]
189
+ : await store.fetchFromSupabase(query) as TRow[]
190
+ const invalidate = store.invalidate ?? noOp
191
+ const subscribe = store.subscribe ?? noOp
192
+ const unsubscribe = store.unsubscribe ?? noOp
193
+
194
+ return {
195
+ items: storeItems,
196
+ totalCount,
197
+ loading,
198
+ error,
199
+ stale,
200
+ getById,
201
+ create,
202
+ update,
203
+ remove,
204
+ fetch,
205
+ refresh,
206
+ invalidate,
207
+ subscribe,
208
+ unsubscribe,
209
+ }
210
+ }
211
+
212
+ // ############################################################
213
+ // # API MODE (stateless)
214
+ // ############################################################
215
+ const api = useSupabaseApi()
216
+ const supabase = useSupabaseClient()
217
+ const supabaseUser = useSupabaseUser()
218
+ const items = ref<TRow[]>([])
219
+ const typedItems = items as unknown as Ref<TRow[]>
220
+ const totalCount = ref<number | null>(null)
221
+ const loading = ref(false)
222
+ const error = ref<unknown>(null)
223
+ const stale = ref(true)
224
+ let fetchSequence = 0
225
+ let collectionRevision = 0
226
+ let currentQuery: RowQuery = {}
227
+ let subscription: ReturnType<typeof supabase.channel> | null = null
228
+ let subscriptionOwnerId: string | null = null
229
+ const normalizedCurrentQuery = () => {
230
+ const order = normalizeOrder(currentQuery.orderBy, currentQuery.orderDirection, currentQuery.orderForeignTable, primaryKey)
231
+ return { ...currentQuery, ...order }
232
+ }
233
+
234
+ const itemKey = (item: TRow) => (item as Record<string, unknown>)[primaryKey]
235
+ const addOrUpdate = (item: TRow) => {
236
+ const id = itemKey(item)
237
+ const index = typedItems.value.findIndex(candidate => itemKey(candidate) === id)
238
+ if (index < 0) typedItems.value = [item, ...typedItems.value]
239
+ else {
240
+ const next = [...typedItems.value]
241
+ next[index] = item
242
+ typedItems.value = next
243
+ }
244
+ }
245
+ const invalidate = () => {
246
+ collectionRevision++
247
+ stale.value = true
248
+ }
249
+ const mutationSucceeded = () => {
250
+ invalidate()
251
+ if (isComplexCollectionQuery(normalizedCurrentQuery())) totalCount.value = null
252
+ }
253
+
254
+ const getById = async (id: string | number, select: string = '*') => {
255
+ const response = await api.getById<TRow>(modelName, id, { key: primaryKey, select })
256
+ if (!response.success) throw response.error
257
+ return (response.data ?? null) as TRow | null
258
+ }
259
+
260
+ const create = async (payload: TInsert) => {
261
+ const response = await api.create<TRow>(modelName, payload as MutationPayload)
262
+ if (!response.success) throw response.error
263
+ const created = (response.data ?? null) as TRow | null
264
+ if (created === null || created === undefined) {
265
+ throw new Error(`Cannot create "${modelName}": Supabase returned no row.`)
266
+ }
267
+ mutationSucceeded()
268
+ const activeQuery = normalizedCurrentQuery()
269
+ if (!isComplexCollectionQuery(activeQuery)) {
270
+ addOrUpdate(created)
271
+ typedItems.value = sortCollection(typedItems.value as Record<string, any>[], activeQuery).slice(0, activeQuery.limit ?? 100) as TRow[]
272
+ if (totalCount.value != null) totalCount.value++
273
+ }
274
+ return created
275
+ }
276
+
277
+ const update = async (target: MutationTarget, payload: TUpdate) => {
278
+ const id = resolveMutationTarget(target, 'update')
279
+ const response = await api.update<TRow>(modelName, id, payload as MutationPayload, { key: primaryKey })
280
+ if (!response.success) throw response.error
281
+ const updated = (Array.isArray(response.data) ? response.data[0] : response.data) as TRow | null
282
+ if (updated) {
283
+ mutationSucceeded()
284
+ const activeQuery = normalizedCurrentQuery()
285
+ if (!isComplexCollectionQuery(activeQuery)) {
286
+ addOrUpdate(updated)
287
+ typedItems.value = sortCollection(typedItems.value as Record<string, any>[], activeQuery) as TRow[]
288
+ }
289
+ }
290
+ return updated ?? null
291
+ }
292
+
293
+ const remove = async (target: MutationTarget) => {
294
+ const id = resolveMutationTarget(target, 'remove')
295
+ const response = await api.remove(modelName, id, { key: primaryKey })
296
+ if (!response.success) throw response.error
297
+ const existed = typedItems.value.some(item => itemKey(item) === id)
298
+ mutationSucceeded()
299
+ typedItems.value = typedItems.value.filter(item => itemKey(item) !== id)
300
+ if (!isComplexCollectionQuery(normalizedCurrentQuery()) && existed && totalCount.value != null) totalCount.value--
301
+ }
302
+
303
+ const fetch = async (query: RowQuery = {}) => {
304
+ const requestId = ++fetchSequence
305
+ const startingRevision = collectionRevision
306
+ currentQuery = query
307
+ loading.value = true
308
+ error.value = null
309
+ const {
310
+ select = '*',
311
+ where,
312
+ limit = 100,
313
+ offset = 0,
314
+ search,
315
+ searchColumns,
316
+ } = query
317
+
318
+ const { orderBy, orderDirection, orderForeignTable } = normalizeOrder(
319
+ query.orderBy,
320
+ query.orderDirection,
321
+ query.orderForeignTable,
322
+ primaryKey,
323
+ )
324
+
325
+ let data: TRow[] = []
326
+ let count: number | null = null
327
+
328
+ try {
329
+ const response = await api.all<TRow>(modelName, {
330
+ select,
331
+ where,
332
+ orderBy,
333
+ orderDirection,
334
+ orderForeignTable,
335
+ limit,
336
+ offset,
337
+ search,
338
+ searchColumns,
339
+ })
340
+ if (!response.success) throw response.error
341
+
342
+ data = (response.data ?? []) as TRow[]
343
+ count = response.count ?? null
344
+
345
+ // Latest request wins: rapid search/filter changes must not let an older
346
+ // response overwrite the state already produced by a newer query.
347
+ if (requestId === fetchSequence && startingRevision === collectionRevision) {
348
+ typedItems.value = Array.isArray(data) ? data : []
349
+ totalCount.value = count ?? null
350
+ stale.value = false
351
+ }
352
+
353
+ return (Array.isArray(data) ? data : []) as TRow[]
354
+ } catch (fetchError) {
355
+ error.value = fetchError
356
+ stale.value = true
357
+ throw fetchError
358
+ } finally {
359
+ if (requestId === fetchSequence) loading.value = false
360
+ }
361
+ }
362
+
363
+ const refresh = (query: RowQuery = currentQuery) => fetch(query)
364
+
365
+ const subscribe = () => {
366
+ if (typeof window === 'undefined' || subscription) return
367
+ subscriptionOwnerId = supabaseUser.value?.id ?? null
368
+ subscription = supabase.channel(`public:${modelName}`)
369
+ .on('postgres_changes', { event: '*', schema: 'public', table: modelName }, (payload: any) => {
370
+ if (subscriptionOwnerId !== (supabaseUser.value?.id ?? null)) return
371
+ invalidate()
372
+ if (isComplexCollectionQuery(normalizedCurrentQuery())) return
373
+ if (payload.eventType === 'DELETE') typedItems.value = typedItems.value.filter(item => itemKey(item) !== payload.old?.[primaryKey])
374
+ else if (payload.new) {
375
+ addOrUpdate(payload.new as TRow)
376
+ typedItems.value = sortCollection(typedItems.value as Record<string, any>[], normalizedCurrentQuery()) as TRow[]
377
+ }
378
+ })
379
+ .subscribe()
380
+ }
381
+ const unsubscribe = async () => {
382
+ const active = subscription
383
+ subscription = null
384
+ subscriptionOwnerId = null
385
+ if (!active) return
386
+ if (typeof (supabase as any).removeChannel === 'function') await (supabase as any).removeChannel(active)
387
+ else await active.unsubscribe()
388
+ }
389
+ watch(() => supabaseUser.value?.id ?? null, () => {
390
+ void unsubscribe()
391
+ typedItems.value = []
392
+ totalCount.value = null
393
+ invalidate()
394
+ })
395
+
396
+ return {
397
+ items: typedItems,
398
+ totalCount,
399
+ loading,
400
+ error,
401
+ stale,
402
+ getById,
403
+ create,
404
+ update,
405
+ remove,
406
+ fetch,
407
+ refresh,
408
+ invalidate,
409
+ subscribe,
410
+ unsubscribe,
411
+ }
412
+ }
@@ -0,0 +1,126 @@
1
+ import type { ListOptions, WhereClause, WhereOperator } from '../types/list'
2
+
3
+ export type QueryBuilderLike = {
4
+ [key: string]: unknown
5
+ or: (expression: string) => QueryBuilderLike
6
+ order: (column: string, options: Record<string, unknown>) => QueryBuilderLike
7
+ range: (from: number, to: number) => QueryBuilderLike
8
+ eq: (column: string, value: unknown) => QueryBuilderLike
9
+ neq: (column: string, value: unknown) => QueryBuilderLike
10
+ gt: (column: string, value: unknown) => QueryBuilderLike
11
+ gte: (column: string, value: unknown) => QueryBuilderLike
12
+ lt: (column: string, value: unknown) => QueryBuilderLike
13
+ lte: (column: string, value: unknown) => QueryBuilderLike
14
+ ilike: (column: string, value: unknown) => QueryBuilderLike
15
+ in: (column: string, value: readonly unknown[]) => QueryBuilderLike
16
+ }
17
+
18
+ export function escapePostgrestSearchTerm(value: string) {
19
+ return value
20
+ .replace(/[\\%_]/g, match => `\\${match}`)
21
+ .replace(/[(),]/g, ' ')
22
+ .trim()
23
+ }
24
+
25
+ export function applySearch<T extends QueryBuilderLike>(query: T, options: ListOptions): T {
26
+ const search = options.search?.trim()
27
+ if (!search) return query
28
+
29
+ const columns = [...new Set((options.searchColumns ?? []).map(column => column.trim()).filter(Boolean))]
30
+ if (columns.length === 0) return query
31
+
32
+ const term = escapePostgrestSearchTerm(search)
33
+ if (!term) return query
34
+
35
+ const expression = columns.map(column => `${column}.ilike.%${term}%`).join(',')
36
+ return query.or(expression) as T
37
+ }
38
+
39
+ export function normalizePagination(options: Pick<ListOptions, 'limit' | 'offset'>) {
40
+ const limit = options.limit ?? 100
41
+ const offset = options.offset ?? 0
42
+
43
+ if (!Number.isSafeInteger(limit) || limit < 1) {
44
+ throw new RangeError('[nsdb] limit must be a positive safe integer.')
45
+ }
46
+ if (!Number.isSafeInteger(offset) || offset < 0) {
47
+ throw new RangeError('[nsdb] offset must be a non-negative safe integer.')
48
+ }
49
+
50
+ return { limit, offset }
51
+ }
52
+
53
+ export function applyListOptions<T extends QueryBuilderLike>(query: T, options: ListOptions): T {
54
+ const orderBy = options.orderBy
55
+ const orderDirection = options.orderDirection ?? 'asc'
56
+ const { limit, offset } = normalizePagination(options)
57
+ let finalQuery: QueryBuilderLike = query
58
+
59
+ if (orderBy && options.orderForeignTable) {
60
+ finalQuery = finalQuery.order(orderBy, {
61
+ ascending: orderDirection === 'asc',
62
+ referencedTable: options.orderForeignTable,
63
+ })
64
+ } else if (orderBy) {
65
+ finalQuery = finalQuery.order(orderBy, { ascending: orderDirection === 'asc' })
66
+ }
67
+
68
+ return finalQuery.range(offset, offset + limit - 1) as T
69
+ }
70
+
71
+ export function applySingleFilter<T extends QueryBuilderLike>(
72
+ query: T,
73
+ column: string,
74
+ filter: WhereOperator,
75
+ ): T {
76
+ switch (filter.op) {
77
+ case 'eq': return query.eq(column, filter.value) as T
78
+ case 'neq': return query.neq(column, filter.value) as T
79
+ case 'gt': return query.gt(column, filter.value) as T
80
+ case 'gte': return query.gte(column, filter.value) as T
81
+ case 'lt': return query.lt(column, filter.value) as T
82
+ case 'lte': return query.lte(column, filter.value) as T
83
+ case 'ilike': return query.ilike(column, filter.value) as T
84
+ case 'in': {
85
+ if (!Array.isArray(filter.value)) {
86
+ throw new TypeError(`[nsdb] The "in" filter for "${column}" requires an array value.`)
87
+ }
88
+ return query.in(column, filter.value) as T
89
+ }
90
+ default: throw new TypeError(`[nsdb] Unsupported filter operator for "${column}".`)
91
+ }
92
+ }
93
+
94
+ export function applyWhereFilters<T extends QueryBuilderLike>(query: T, where?: WhereClause): T {
95
+ if (!where || Object.keys(where).length === 0) return query
96
+
97
+ let finalQuery: QueryBuilderLike = query
98
+ for (const [column, rawValue] of Object.entries(where)) {
99
+ if (Array.isArray(rawValue)) {
100
+ const containsOperators = rawValue.some(
101
+ part => part != null && typeof part === 'object' && 'op' in part,
102
+ )
103
+ if (!containsOperators) {
104
+ finalQuery = finalQuery.in(column, rawValue)
105
+ continue
106
+ }
107
+
108
+ for (const part of rawValue) {
109
+ if (!part || typeof part !== 'object' || !('op' in part)) {
110
+ throw new TypeError(`[nsdb] Filter array for "${column}" cannot mix values and operators.`)
111
+ }
112
+ finalQuery = applySingleFilter(finalQuery, column, part as WhereOperator)
113
+ }
114
+ continue
115
+ }
116
+
117
+ if (rawValue && typeof rawValue === 'object' && 'op' in rawValue) {
118
+ finalQuery = applySingleFilter(finalQuery, column, rawValue as WhereOperator)
119
+ continue
120
+ }
121
+
122
+ finalQuery = finalQuery.eq(column, rawValue)
123
+ }
124
+
125
+ return finalQuery as T
126
+ }