@cuboapp/api-backend 1.0.38 → 3.0.1

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/src/index.ts CHANGED
@@ -1,444 +1,7 @@
1
- import { QueryTypes } from '@cuboapp/database'
2
- import { CuboApiEntitiesMap } from '@cuboapp/types'
3
- import { cloneDeep } from '@cuboapp/utils'
4
-
5
- import { ApiHelpers } from './helpers'
6
- import { CuboS3UploadFunctionOptions, CuboS3UploadFunctionResult, uploadToS3 } from './s3'
7
- import { CuboBackendApiAuth, CuboBackendApiOptions, CuboCrudGetManyResponse, CuboCrudMethodOptions, CuboCrudRequest } from './types'
8
-
9
- export class CuboBackendApi<T extends CuboApiEntitiesMap<T>, A extends unknown> {
10
- constructor(public options: CuboBackendApiOptions<T, A>) {}
11
-
12
- public auth: CuboBackendApiAuth
13
- public helpers = new ApiHelpers<T, A>(this)
14
-
15
- async init() {
16
- try {
17
- if (this.options.api !== undefined) {
18
- this.auth = await this.request<{ variables: Record<string, any> }>('/auth/me?with=variables', {
19
- withAuth: true
20
- })
21
-
22
- if (!this.auth) {
23
- throw { code: 403, text: 'Авторизация Cubo-Backend не пройдена' }
24
- }
25
-
26
- if (this.auth.variables?.db !== undefined) {
27
- this.options.db = this.options.db || {}
28
-
29
- this.options.db.options = {
30
- ...(this.options.db.options || {}),
31
- ...(this.auth.variables?.db || {})
32
- }
33
- }
34
- }
35
- } catch (e) {
36
- console.error('[API-BACKEND] startup error', e)
37
- }
38
- }
39
-
40
- async destroy() {
41
- const read = await this.helpers.getConnection('read', false)
42
- const write = await this.helpers.getConnection('write', false)
43
-
44
- if (read) {
45
- await read.disconnect()
46
- }
47
-
48
- if (write) {
49
- await write.disconnect()
50
- }
51
- }
52
-
53
- private debounces = new Map<`${string}:${number}`, { started: number; promise: Promise<any> }>()
54
-
55
- public async transaction() {
56
- const db = await this.helpers.getConnection('write')
57
-
58
- return db.connection.transaction()
59
- }
60
-
61
- public async request<T>(url: string, opts?: RequestInit & { debug?: boolean; withAuth?: boolean; withoutContentType?: boolean }) {
62
- const baseUrl = this.options?.api?.base_url || 'https://api.cubo.sh'
63
-
64
- const headers: Record<string, any> = {
65
- ...(opts?.headers || {})
66
- }
67
-
68
- if (opts?.withAuth) {
69
- Object.assign(headers, this.options.api?.headers || {})
70
- }
71
-
72
- if (!headers['Content-Type'] && !opts?.withoutContentType) {
73
- headers['Accept'] = 'application/json'
74
- headers['Content-Type'] = 'application/json'
75
- }
76
-
77
- const requestUrl = baseUrl.replace(/\/$/, '') + '/' + url.replace(/^\//, '')
78
- const requestOpts = {
79
- ...(opts || {}),
80
- headers
81
- }
82
-
83
- try {
84
- const response = await fetch(requestUrl, requestOpts)
85
-
86
- if ([201, 200].includes(response.status)) {
87
- try {
88
- const json = await response.json()
89
-
90
- return json as Promise<T>
91
- } catch (e) {
92
- const text = await response.text()
93
-
94
- try {
95
- const json = text ? JSON.parse(text) : null
96
-
97
- return json as Promise<T>
98
- } catch {
99
- throw { status: 500, error: 'Invalid response', text }
100
- }
101
- }
102
- } else {
103
- throw {
104
- status: response.status,
105
- error: response.statusText,
106
- text: await response.text()
107
- }
108
- }
109
- } catch (e) {
110
- if (opts?.debug) {
111
- console.log(
112
- '[API BACKEND]',
113
- JSON.stringify(
114
- {
115
- request: {
116
- url: requestUrl,
117
- opts: requestOpts
118
- },
119
- error: e
120
- },
121
- null,
122
- 2
123
- )
124
- )
125
- }
126
-
127
- throw e
128
- }
129
- }
130
-
131
- public async uploadToS3(buffer: Buffer, opts: CuboS3UploadFunctionOptions): Promise<CuboS3UploadFunctionResult> {
132
- return uploadToS3(buffer, opts)
133
- }
134
-
135
- private async getEntity(entityAlias: Extract<keyof T, string>) {
136
- const entity = await this.helpers.getEntityByAlias(entityAlias)
137
- if (!entity) {
138
- throw new Error('Entity no found: "' + String(entityAlias) + '"')
139
- }
140
-
141
- return entity
142
- }
143
-
144
- async getMany<K extends Extract<keyof T, string> = Extract<keyof T, string>>(
145
- entityAlias: K,
146
- req?: CuboCrudRequest,
147
- opts?: CuboCrudMethodOptions
148
- ): Promise<CuboCrudGetManyResponse<T[K]>> {
149
- opts = opts || {}
150
- req = req || {}
151
-
152
- req.query = cloneDeep(req.query || {})
153
-
154
- const entity = await this.getEntity(entityAlias)
155
- const augmentation = this.options?.augmentations?.[entityAlias]
156
-
157
- if (augmentation?.beforeGetMany) {
158
- opts.queryOptions = await augmentation.beforeGetMany(req, opts)
159
- }
160
-
161
- // get all joins
162
- const queryDto = await this.helpers.withes.prepare(req, entity, opts)
163
- queryDto.withDeleted = opts.queryOptions?.withDeleted
164
-
165
- // create find query
166
- const regularQuery = await this.helpers.createFindQuery(req, entity, queryDto)
167
- const countQuery = await this.helpers.createFindQuery(req, entity, queryDto, true)
168
-
169
- const db = await this.helpers.getConnection('write')
170
-
171
- if (opts?.log) {
172
- console.log(`QUERY:getMany to "${String(entityAlias)}":`, regularQuery.sql, regularQuery.replacements, { req, opts, queryDto })
173
- }
174
-
175
- const rows = await db.connection.query<any>(regularQuery.sql!, {
176
- type: QueryTypes.SELECT,
177
- replacements: regularQuery.replacements,
178
- transaction: opts?.transaction
179
- })
180
- const [totals] = await db.connection.query<any>(countQuery.sql!, {
181
- type: QueryTypes.SELECT,
182
- replacements: countQuery.replacements,
183
- transaction: opts?.transaction
184
- })
185
-
186
- const response = {
187
- rows: await this.helpers.convert.prepareAll(req, entity, rows, regularQuery),
188
- totals: {
189
- count: totals.total !== undefined ? +totals.total : 0
190
- }
191
- }
192
-
193
- if (augmentation?.afterGetMany && !opts?.excludeHooks?.includes('afterGetMany')) {
194
- return augmentation.afterGetMany(response, req, opts)
195
- }
196
-
197
- return response
198
- }
199
-
200
- async getOne<K extends Extract<keyof T, string> = Extract<keyof T, string>>(
201
- entityAlias: K,
202
- req: CuboCrudRequest,
203
- opts?: CuboCrudMethodOptions
204
- ): Promise<T[K] | undefined> {
205
- opts = opts || {}
206
-
207
- const entity = await this.getEntity(entityAlias)
208
- const augmentation = this.options?.augmentations?.[entityAlias]
209
- if (augmentation?.beforeGetOne) {
210
- opts.queryOptions = await augmentation.beforeGetOne(req, opts)
211
- }
212
-
213
- // get all joins
214
- const queryDto = await this.helpers.withes.prepare(req, entity, opts)
215
- queryDto.withDeleted = opts.queryOptions?.withDeleted
216
- queryDto.limit = 1
217
-
218
- const query = await this.helpers.createFindQuery(req, entity, queryDto)
219
-
220
- const db = await this.helpers.getConnection('read')
221
-
222
- const [row] = await db.connection.query<any>(query.sql!, {
223
- type: QueryTypes.SELECT,
224
- replacements: query.replacements,
225
- transaction: opts?.transaction
226
- })
227
-
228
- if (opts?.log) {
229
- console.log(`QUERY:getOne to "${String(entityAlias)}":`, query.sql, query.replacements)
230
- }
231
-
232
- if (!row) {
233
- return null as any
234
- // throw { status: 404, message: 'Element "' + entityAlias + '" not found', details: req }
235
- }
236
-
237
- const response = await this.helpers.convert.prepareOne(req, entity, row, query)
238
-
239
- if (augmentation?.afterGetOne && !opts?.excludeHooks?.includes('afterGetOne')) {
240
- return augmentation.afterGetOne(response, req, opts)
241
- }
242
-
243
- return response
244
- }
245
-
246
- async createOne<K extends Extract<keyof T, string> = Extract<keyof T, string>>(
247
- entityAlias: K,
248
- req: CuboCrudRequest,
249
- opts?: CuboCrudMethodOptions
250
- ): Promise<T[K]> {
251
- opts = opts || {}
252
- const entity = await this.helpers.getEntityByAlias(entityAlias)
253
- const augmentation = this.options?.augmentations?.[entityAlias]
254
-
255
- if (augmentation?.beforeCreate) {
256
- opts.queryOptions = await augmentation.beforeCreate(req, opts)
257
- }
258
-
259
- const dto = await this.helpers.data.prepare('create', entity!.fields, req.body, opts?.performer_id || 0)
260
- if (!Object.keys(dto)) {
261
- throw { status: 400, text: 'No keys to update' }
262
- }
263
-
264
- // console.log('dto', dto)
265
- // const prepared = dbPrepareInsertQueryString(dto)
266
- const db = await this.helpers.getConnection('write')
267
- const [created_id] = await db.create(entity.alias, dto, { transaction: opts?.transaction, log: opts?.log })
268
-
269
- if (!created_id) {
270
- throw { status: 400, text: 'Unable to create entity' }
271
- }
272
-
273
- let response = await this.getOne<K>(entityAlias, { query: { id: created_id } }, opts)
274
-
275
- if (!response) {
276
- throw { status: 400, text: 'Unable to find entity after create' }
277
- }
278
-
279
- if (augmentation?.afterCreate && !opts?.excludeHooks?.includes('afterCreate')) {
280
- response = await augmentation.afterCreate(response, req, opts)
281
- }
282
-
283
- if (this.options.hooks?.onAfterCreate && !opts?.excludeHooks?.includes('baseHooks')) {
284
- await this.options.hooks.onAfterCreate(entity, response, opts)
285
- }
286
-
287
- return response
288
- }
289
-
290
- async updateOne<K extends Extract<keyof T, string> = Extract<keyof T, string>>(
291
- entityAlias: K,
292
- req: CuboCrudRequest,
293
- opts?: CuboCrudMethodOptions
294
- ): Promise<T[K] | undefined> {
295
- if (opts?.debounce) {
296
- if (!req.query?.id) {
297
- throw 'Debounce "updateOne" allowed only with "id"'
298
- }
299
-
300
- const key = `${entityAlias}:${+req.query.id}` as `${string}:${number}`
301
- let ex = this.debounces.get(key)
302
- if (ex) {
303
- if (ex.started > +new Date() - opts.debounce) {
304
- Promise.reject(ex.promise)
305
- } else {
306
- return ex.promise
307
- }
308
- }
309
-
310
- return new Promise((resolve, reject) => {
311
- setTimeout(() => {
312
- this.updateOne(entityAlias, req, { ...(opts || {}), debounce: undefined })
313
- .then(resolve)
314
- .catch(reject)
315
- }, opts.debounce)
316
- })
317
- }
318
-
319
- opts = opts || {}
320
- const entity = await this.helpers.getEntityByAlias(entityAlias)
321
- const augmentation = this.options?.augmentations?.[entityAlias]
322
-
323
- if (augmentation?.beforeUpdate && !opts?.excludeHooks?.includes('beforeUpdate')) {
324
- opts.queryOptions = await augmentation.beforeUpdate(req, opts)
325
- }
326
-
327
- const item: any = await this.getOne(entityAlias, req, opts)
328
-
329
- if (!item) {
330
- throw { status: 404, text: 'Entity not found' }
331
- }
332
-
333
- const initialEntity = cloneDeep(item)
334
-
335
- try {
336
- if (!!augmentation?.can) {
337
- await augmentation.can('update', { entity, row: item, auth: opts?.extra?.auth, req })
338
- }
339
- } catch {
340
- throw { status: 403, text: 'Updating entity forbidden' }
341
- }
342
-
343
- const dto = await this.helpers.data.prepare('update', entity!.fields, req.body, opts?.performer_id || 0)
344
- if (!Object.keys(dto)) {
345
- throw { status: 400, text: 'No keys to update' }
346
- }
347
-
348
- const db = await this.helpers.getConnection('write')
349
- await db.update(entity.alias, 'id = :id', { id: item.id }, dto, { transaction: opts?.transaction, log: opts?.log })
350
-
351
- let response = await this.getOne<K>(entityAlias, req, opts)
352
- if (!response) {
353
- throw { status: 400, text: 'Unable to find entity after update' }
354
- }
355
-
356
- if (augmentation?.afterUpdate && !opts?.excludeHooks?.includes('afterUpdate')) {
357
- response = await augmentation.afterUpdate(response, req, opts)
358
- }
359
-
360
- if (this.options.hooks?.onAfterUpdate && !opts?.excludeHooks?.includes('baseHooks')) {
361
- await this.options.hooks.onAfterUpdate(entity, response, initialEntity, opts)
362
- }
363
-
364
- return response
365
- }
366
-
367
- async deleteOne<K extends Extract<keyof T, string>>(
368
- entityAlias: K,
369
- req: CuboCrudRequest,
370
- opts?: CuboCrudMethodOptions
371
- ): Promise<boolean> {
372
- opts = opts || {}
373
- const entity = await this.helpers.getEntityByAlias(entityAlias)
374
- const augmentation = this.options?.augmentations?.[entityAlias]
375
-
376
- if (augmentation?.beforeDelete) {
377
- opts.queryOptions = await augmentation.beforeDelete(req, opts)
378
- }
379
-
380
- const item: any = await this.getOne(entityAlias, req, opts)
381
- if (!item) {
382
- throw { status: 404, text: 'Entity not found' }
383
- }
384
-
385
- const dto = await this.helpers.data.prepare('delete', entity!.fields, req.body || {}, opts?.performer_id)
386
- if (!Object.keys(dto)) {
387
- throw { status: 400, text: 'No keys to update' }
388
- }
389
-
390
- const db = await this.helpers.getConnection('write')
391
- await db.update(entity.alias, 'id = :id', { id: item.id }, dto, { transaction: opts?.transaction, log: opts?.log })
392
-
393
- if (this.options.hooks?.onAfterDelete && !opts?.excludeHooks?.includes('baseHooks')) {
394
- await this.options.hooks.onAfterDelete(entity, item, opts)
395
- }
396
-
397
- // console.log(entityAlias, augmentation, opts)
398
-
399
- if (augmentation?.afterDelete && !opts?.excludeHooks?.includes('afterDelete')) {
400
- return augmentation.afterDelete(item, req, opts)
401
- }
402
-
403
- return true
404
- }
405
-
406
- async deleteMany<K extends Extract<keyof T, string>>(
407
- entityAlias: K,
408
- req: CuboCrudRequest,
409
- opts?: CuboCrudMethodOptions
410
- ): Promise<boolean> {
411
- opts = opts || {}
412
- const entity = await this.helpers.getEntityByAlias(entityAlias)
413
- const augmentation = this.options?.augmentations?.[entityAlias]
414
-
415
- if (augmentation?.beforeDelete) {
416
- opts.queryOptions = await augmentation.beforeDelete(req, opts)
417
- }
418
-
419
- const { rows: items } = await this.getMany(entityAlias, req, opts)
420
- if (!items) {
421
- return false
422
- }
423
-
424
- const dto = await this.helpers.data.prepare('delete', entity!.fields, req.body || {}, opts?.performer_id)
425
- if (!Object.keys(dto)) {
426
- throw { status: 400, text: 'No keys to update' }
427
- }
428
-
429
- const db = await this.helpers.getConnection('write')
430
-
431
- for (const item of items) {
432
- await db.update(entity.alias, 'id = :id', { id: (item as any).id }, dto, { transaction: opts?.transaction, log: opts?.log })
433
-
434
- if (this.options.hooks?.onAfterDelete && !opts?.excludeHooks?.includes('baseHooks')) {
435
- await this.options.hooks.onAfterDelete(entity, item, opts)
436
- }
437
- }
438
-
439
- return true
440
- }
441
- }
442
-
443
- export * from './helpers'
1
+ export * from './core'
444
2
  export * from './types'
3
+ export * from './helpers'
4
+ export * from './hooks'
5
+ export * from './query'
6
+ export * from './dialects'
7
+ export * from './crdt'
@@ -0,0 +1,212 @@
1
+ import { CuboCompiledQuery, CuboOp, CuboQuery, CuboSort, CuboWith } from './types'
2
+
3
+ /**
4
+ * Компилирует типизированный CuboQuery<R> в плоский «строковый» запрос движка
5
+ * (CuboCrudRequest.query). Чистая функция: на каждый вызов создаётся новый
6
+ * объект — query-опции иммутабельны по построению, ничего не «накапливается».
7
+ *
8
+ * Поддержка операторов через flat-DSL:
9
+ * eq/ne/gt/gte/lt/lte/in/nin/like/ilike/between/isNull/not.
10
+ * Диапазон gte+lte на одном поле автоматически сворачивается в between.
11
+ * Экзотические комбинации (>1 оператора, не сводящиеся к диапазону) —
12
+ * через escape-hatch `raw` в CuboCrudMethodOptions.queryOptions.
13
+ */
14
+ export function compileQuery<R>(query?: CuboQuery<R>): CuboCompiledQuery {
15
+ const out: CuboCompiledQuery = {}
16
+
17
+ if (!query) {
18
+ return out
19
+ }
20
+
21
+ if (query.where) {
22
+ compileWhere(query.where as Record<string, any>, '', out)
23
+ }
24
+
25
+ if (query.with) {
26
+ const withList = compileWith(query.with as CuboWith<R>)
27
+ if (withList.length) {
28
+ out.with = withList.join(',')
29
+ }
30
+ }
31
+
32
+ if (query.sort) {
33
+ const sort = compileSort(query.sort)
34
+ if (sort) {
35
+ out.sort = sort
36
+ }
37
+ }
38
+
39
+ if (query.limit !== undefined) {
40
+ out.limit = query.limit
41
+ }
42
+
43
+ if (query.page !== undefined) {
44
+ out.page = query.page
45
+ }
46
+
47
+ return out
48
+ }
49
+
50
+ function compileWhere(where: Record<string, any>, prefix: string, out: CuboCompiledQuery) {
51
+ for (const key in where) {
52
+ const value = where[key]
53
+ const fullKey = prefix ? `${prefix}.${key}` : key
54
+
55
+ // вложенная связь -> рекурсивно с dot-notation ключом
56
+ if (isPlainObject(value) && !isOperatorObject(value)) {
57
+ compileWhere(value, fullKey, out)
58
+ continue
59
+ }
60
+
61
+ out[fullKey] = compileOp(fullKey, value)
62
+ }
63
+ }
64
+
65
+ /** true, если объект похож на набор операторов (а не на вложенный where). */
66
+ const OP_KEYS = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'like', 'ilike', 'between', 'isNull', 'not']
67
+ function isOperatorObject(value: object): boolean {
68
+ return Object.keys(value).some((k) => OP_KEYS.includes(k))
69
+ }
70
+
71
+ function isPlainObject(value: any): value is Record<string, any> {
72
+ return value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)
73
+ }
74
+
75
+ /** Кодирует один оператор поля в строковое значение flat-DSL. */
76
+ function compileOp(key: string, op: CuboOp<any>): any {
77
+ // короткая форма: просто значение (или null) = eq
78
+ if (op === null || typeof op !== 'object' || op instanceof Date || Array.isArray(op)) {
79
+ return op
80
+ }
81
+
82
+ const o = op as Exclude<CuboOp<any>, any[]> & Record<string, any>
83
+ const present = Object.keys(o).filter((k) => o[k] !== undefined)
84
+
85
+ // диапазон gte+lte -> between
86
+ if (present.length === 2 && present.includes('gte') && present.includes('lte')) {
87
+ return `btw:${o.gte}:${o.lte}`
88
+ }
89
+
90
+ if (present.length > 1) {
91
+ throw new Error(`query "${key}": несколько операторов на одно поле не поддерживаются (используйте between или queryOptions.raw): ${present.join(', ')}`)
92
+ }
93
+
94
+ const [cond] = present
95
+
96
+ switch (cond) {
97
+ case 'eq':
98
+ return o.eq
99
+ case 'ne':
100
+ return o.ne === null ? 'not:null' : `not:${o.ne}`
101
+ case 'gt':
102
+ return `gt:${o.gt}`
103
+ case 'gte':
104
+ return `gte:${o.gte}`
105
+ case 'lt':
106
+ return `lt:${o.lt}`
107
+ case 'lte':
108
+ return `lte:${o.lte}`
109
+ case 'in':
110
+ return `in:${(o.in as any[]).join(',')}`
111
+ case 'nin':
112
+ return `nin:${(o.nin as any[]).join(',')}`
113
+ case 'like':
114
+ return `like:${o.like}`
115
+ case 'ilike':
116
+ return `ilike:${o.ilike}`
117
+ case 'between':
118
+ return `btw:${o.between[0]}:${o.between[1]}`
119
+ case 'isNull':
120
+ return o.isNull ? null : 'not:null'
121
+ case 'not':
122
+ return compileNot(key, o.not)
123
+ default:
124
+ // пустой объект операторов — игнорируем
125
+ return undefined
126
+ }
127
+ }
128
+
129
+ /** Кодирует отрицание. Движок поддерживает максимум 3 уровня: not:<cond>:<value>. */
130
+ function compileNot(key: string, inner: CuboOp<any>): string {
131
+ if (inner === null) {
132
+ return 'not:null'
133
+ }
134
+ if (typeof inner !== 'object' || inner instanceof Date || Array.isArray(inner)) {
135
+ return `not:${inner}` // not:value === ne
136
+ }
137
+
138
+ const o = inner as Record<string, any>
139
+ const present = Object.keys(o).filter((k) => o[k] !== undefined)
140
+ if (present.length !== 1) {
141
+ throw new Error(`query "${key}": "not" поддерживает ровно один оператор`)
142
+ }
143
+
144
+ const [cond] = present
145
+ switch (cond) {
146
+ case 'eq':
147
+ return o.eq === null ? 'not:null' : `not:${o.eq}`
148
+ case 'gt':
149
+ return `not:gt:${o.gt}`
150
+ case 'gte':
151
+ return `not:gte:${o.gte}`
152
+ case 'lt':
153
+ return `not:lt:${o.lt}`
154
+ case 'lte':
155
+ return `not:lte:${o.lte}`
156
+ case 'in':
157
+ return `not:in:${(o.in as any[]).join(',')}`
158
+ case 'nin':
159
+ return `not:nin:${(o.nin as any[]).join(',')}`
160
+ case 'like':
161
+ return `not:like:${o.like}`
162
+ case 'ilike':
163
+ return `not:ilike:${o.ilike}`
164
+ case 'isNull':
165
+ return o.isNull ? 'not:null' : null!
166
+ default:
167
+ throw new Error(`query "${key}": оператор "${cond}" нельзя инвертировать через "not" (используйте queryOptions.raw)`)
168
+ }
169
+ }
170
+
171
+ /** Собирает список dot-notation путей для `with` из массива или дерева. */
172
+ function compileWith<R>(input: CuboWith<R>, prefix = ''): string[] {
173
+ const result: string[] = []
174
+
175
+ if (Array.isArray(input)) {
176
+ for (const key of input) {
177
+ result.push(prefix ? `${prefix}.${String(key)}` : String(key))
178
+ }
179
+ return result
180
+ }
181
+
182
+ for (const key in input as Record<string, any>) {
183
+ const node = (input as Record<string, any>)[key]
184
+ const path = prefix ? `${prefix}.${key}` : key
185
+
186
+ result.push(path)
187
+
188
+ if (node && node !== true && node.with) {
189
+ result.push(...compileWith(node.with, path))
190
+ }
191
+ }
192
+
193
+ return result
194
+ }
195
+
196
+ function compileSort<R>(sort: CuboSort<R> | CuboSort<R>[]): string {
197
+ const list = Array.isArray(sort) ? sort : [sort]
198
+ const parts: string[] = []
199
+
200
+ for (const item of list) {
201
+ if (typeof item === 'string') {
202
+ parts.push(item)
203
+ } else if (item && typeof item === 'object') {
204
+ for (const key in item as Record<string, any>) {
205
+ const dir = (item as Record<string, any>)[key]
206
+ parts.push(dir === 'desc' ? `-${key}` : key)
207
+ }
208
+ }
209
+ }
210
+
211
+ return parts.join(',')
212
+ }
@@ -0,0 +1,2 @@
1
+ export * from './types'
2
+ export * from './compile'