@delma/fylo 1.1.2 → 2.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.
Files changed (66) hide show
  1. package/README.md +141 -62
  2. package/eslint.config.js +8 -4
  3. package/package.json +9 -7
  4. package/src/CLI +16 -14
  5. package/src/adapters/cipher.ts +12 -6
  6. package/src/adapters/redis.ts +193 -123
  7. package/src/adapters/s3.ts +6 -12
  8. package/src/core/collection.ts +5 -0
  9. package/src/core/directory.ts +120 -151
  10. package/src/core/extensions.ts +4 -2
  11. package/src/core/format.ts +390 -419
  12. package/src/core/parser.ts +167 -142
  13. package/src/core/query.ts +31 -26
  14. package/src/core/walker.ts +68 -61
  15. package/src/core/write-queue.ts +7 -4
  16. package/src/engines/s3-files.ts +1068 -0
  17. package/src/engines/types.ts +21 -0
  18. package/src/index.ts +754 -378
  19. package/src/migrate-cli.ts +22 -0
  20. package/src/migrate.ts +74 -0
  21. package/src/types/bun-runtime.d.ts +73 -0
  22. package/src/types/fylo.d.ts +115 -27
  23. package/src/types/node-runtime.d.ts +61 -0
  24. package/src/types/query.d.ts +6 -2
  25. package/src/types/vendor-modules.d.ts +8 -7
  26. package/src/worker.ts +7 -1
  27. package/src/workers/write-worker.ts +25 -24
  28. package/tests/collection/truncate.test.js +35 -0
  29. package/tests/{data.ts → data.js} +8 -21
  30. package/tests/{index.ts → index.js} +4 -9
  31. package/tests/integration/aws-s3-files.canary.test.js +22 -0
  32. package/tests/integration/{create.test.ts → create.test.js} +13 -31
  33. package/tests/integration/delete.test.js +95 -0
  34. package/tests/integration/{edge-cases.test.ts → edge-cases.test.js} +50 -124
  35. package/tests/integration/{encryption.test.ts → encryption.test.js} +20 -65
  36. package/tests/integration/{export.test.ts → export.test.js} +8 -23
  37. package/tests/integration/{join-modes.test.ts → join-modes.test.js} +37 -104
  38. package/tests/integration/migration.test.js +38 -0
  39. package/tests/integration/nested.test.js +142 -0
  40. package/tests/integration/operators.test.js +122 -0
  41. package/tests/integration/{queue.test.ts → queue.test.js} +24 -40
  42. package/tests/integration/read.test.js +119 -0
  43. package/tests/integration/rollback.test.js +60 -0
  44. package/tests/integration/s3-files.test.js +192 -0
  45. package/tests/integration/update.test.js +99 -0
  46. package/tests/mocks/{cipher.ts → cipher.js} +11 -26
  47. package/tests/mocks/redis.js +123 -0
  48. package/tests/mocks/{s3.ts → s3.js} +24 -58
  49. package/tests/schemas/album.json +1 -1
  50. package/tests/schemas/comment.json +1 -1
  51. package/tests/schemas/photo.json +1 -1
  52. package/tests/schemas/post.json +1 -1
  53. package/tests/schemas/tip.json +1 -1
  54. package/tests/schemas/todo.json +1 -1
  55. package/tests/schemas/user.d.ts +12 -12
  56. package/tests/schemas/user.json +1 -1
  57. package/tsconfig.json +4 -2
  58. package/tsconfig.typecheck.json +31 -0
  59. package/tests/collection/truncate.test.ts +0 -56
  60. package/tests/integration/delete.test.ts +0 -147
  61. package/tests/integration/nested.test.ts +0 -212
  62. package/tests/integration/operators.test.ts +0 -167
  63. package/tests/integration/read.test.ts +0 -203
  64. package/tests/integration/rollback.test.ts +0 -105
  65. package/tests/integration/update.test.ts +0 -130
  66. package/tests/mocks/redis.ts +0 -169
@@ -1,5 +1,3 @@
1
-
2
-
3
1
  // Token types for SQL lexing
4
2
  enum TokenType {
5
3
  CREATE = 'CREATE',
@@ -78,16 +76,16 @@ class SQLLexer {
78
76
  let result = ''
79
77
  const quote = this.current
80
78
  this.advance() // Skip opening quote
81
-
79
+
82
80
  while (this.current && this.current !== quote) {
83
81
  result += this.current
84
82
  this.advance()
85
83
  }
86
-
84
+
87
85
  if (this.current === quote) {
88
86
  this.advance() // Skip closing quote
89
87
  }
90
-
88
+
91
89
  return result
92
90
  }
93
91
 
@@ -111,32 +109,32 @@ class SQLLexer {
111
109
 
112
110
  private getKeywordType(word: string): TokenType {
113
111
  const keywords: Record<string, TokenType> = {
114
- 'SELECT': TokenType.SELECT,
115
- 'FROM': TokenType.FROM,
116
- 'WHERE': TokenType.WHERE,
117
- 'INSERT': TokenType.INSERT,
118
- 'INTO': TokenType.INTO,
119
- 'VALUES': TokenType.VALUES,
120
- 'UPDATE': TokenType.UPDATE,
121
- 'SET': TokenType.SET,
122
- 'DELETE': TokenType.DELETE,
123
- 'JOIN': TokenType.JOIN,
124
- 'INNER': TokenType.INNER,
125
- 'LEFT': TokenType.LEFT,
126
- 'RIGHT': TokenType.RIGHT,
127
- 'OUTER': TokenType.OUTER,
128
- 'ON': TokenType.ON,
129
- 'GROUP': TokenType.GROUP,
130
- 'BY': TokenType.BY,
131
- 'ORDER': TokenType.ORDER,
132
- 'LIMIT': TokenType.LIMIT,
133
- 'AS': TokenType.AS,
134
- 'AND': TokenType.AND,
135
- 'OR': TokenType.OR,
136
- 'LIKE': TokenType.LIKE,
137
- 'TRUE': TokenType.BOOLEAN,
138
- 'FALSE': TokenType.BOOLEAN,
139
- 'NULL': TokenType.NULL
112
+ SELECT: TokenType.SELECT,
113
+ FROM: TokenType.FROM,
114
+ WHERE: TokenType.WHERE,
115
+ INSERT: TokenType.INSERT,
116
+ INTO: TokenType.INTO,
117
+ VALUES: TokenType.VALUES,
118
+ UPDATE: TokenType.UPDATE,
119
+ SET: TokenType.SET,
120
+ DELETE: TokenType.DELETE,
121
+ JOIN: TokenType.JOIN,
122
+ INNER: TokenType.INNER,
123
+ LEFT: TokenType.LEFT,
124
+ RIGHT: TokenType.RIGHT,
125
+ OUTER: TokenType.OUTER,
126
+ ON: TokenType.ON,
127
+ GROUP: TokenType.GROUP,
128
+ BY: TokenType.BY,
129
+ ORDER: TokenType.ORDER,
130
+ LIMIT: TokenType.LIMIT,
131
+ AS: TokenType.AS,
132
+ AND: TokenType.AND,
133
+ OR: TokenType.OR,
134
+ LIKE: TokenType.LIKE,
135
+ TRUE: TokenType.BOOLEAN,
136
+ FALSE: TokenType.BOOLEAN,
137
+ NULL: TokenType.NULL
140
138
  }
141
139
  return keywords[word.toUpperCase()] || TokenType.IDENTIFIER
142
140
  }
@@ -146,7 +144,7 @@ class SQLLexer {
146
144
 
147
145
  while (this.current) {
148
146
  this.skipWhitespace()
149
-
147
+
150
148
  if (!this.current) break
151
149
 
152
150
  const position = this.position
@@ -169,7 +167,11 @@ class SQLLexer {
169
167
  if (/[a-zA-Z_]/.test(this.current)) {
170
168
  let value = this.readIdentifier()
171
169
  // Support dot notation for nested fields (e.g. address.city → address/city)
172
- while (this.current === '.' && this.position + 1 < this.input.length && /[a-zA-Z_]/.test(this.input[this.position + 1])) {
170
+ while (
171
+ this.current === '.' &&
172
+ this.position + 1 < this.input.length &&
173
+ /[a-zA-Z_]/.test(this.input[this.position + 1])
174
+ ) {
173
175
  this.advance() // skip '.'
174
176
  value += '/' + this.readIdentifier()
175
177
  }
@@ -257,7 +259,11 @@ class SQLParser {
257
259
 
258
260
  private advance(): void {
259
261
  this.position++
260
- this.current = this.tokens[this.position] || { type: TokenType.EOF, value: '', position: -1 }
262
+ this.current = this.tokens[this.position] || {
263
+ type: TokenType.EOF,
264
+ value: '',
265
+ position: -1
266
+ }
261
267
  }
262
268
 
263
269
  private expect(type: TokenType): Token {
@@ -312,7 +318,7 @@ class SQLParser {
312
318
  this.advance()
313
319
  return op ?? ''
314
320
  }
315
-
321
+
316
322
  throw new Error(`Unknown operator: ${this.current.type}`)
317
323
  }
318
324
 
@@ -320,14 +326,14 @@ class SQLParser {
320
326
  const column = this.expect(TokenType.IDENTIFIER).value
321
327
  const operator = this.parseOperator()
322
328
  const value = this.parseValue()
323
-
329
+
324
330
  return { column, operator, value }
325
331
  }
326
332
 
327
333
  private parseWhereClause<T>(): Array<_op<T>> {
328
334
  this.expect(TokenType.WHERE)
329
335
  const conditions: Array<_op<T>> = []
330
-
336
+
331
337
  do {
332
338
  const condition = this.parseCondition()
333
339
  const op: _op<T> = {
@@ -336,26 +342,26 @@ class SQLParser {
336
342
  } as _operand
337
343
  } as _op<T>
338
344
  conditions.push(op)
339
-
345
+
340
346
  if (this.match(TokenType.AND, TokenType.OR)) {
341
347
  this.advance()
342
348
  } else {
343
349
  break
344
350
  }
345
351
  } while (true)
346
-
352
+
347
353
  return conditions
348
354
  }
349
355
 
350
356
  private parseSelectClause(): string[] {
351
357
  this.expect(TokenType.SELECT)
352
358
  const columns: string[] = []
353
-
359
+
354
360
  if (this.current.type === TokenType.ASTERISK) {
355
361
  this.advance()
356
362
  return ['*']
357
363
  }
358
-
364
+
359
365
  do {
360
366
  columns.push(this.expect(TokenType.IDENTIFIER).value)
361
367
  if (this.current.type === TokenType.COMMA) {
@@ -364,7 +370,7 @@ class SQLParser {
364
370
  break
365
371
  }
366
372
  } while (true)
367
-
373
+
368
374
  return columns
369
375
  }
370
376
 
@@ -372,131 +378,139 @@ class SQLParser {
372
378
  const select = this.parseSelectClause()
373
379
  this.expect(TokenType.FROM)
374
380
  const collection = this.expect(TokenType.IDENTIFIER).value
375
-
381
+
376
382
  // Check if this is a JOIN query
377
- if (this.match(TokenType.JOIN, TokenType.INNER, TokenType.LEFT, TokenType.RIGHT, TokenType.OUTER)) {
383
+ if (
384
+ this.match(
385
+ TokenType.JOIN,
386
+ TokenType.INNER,
387
+ TokenType.LEFT,
388
+ TokenType.RIGHT,
389
+ TokenType.OUTER
390
+ )
391
+ ) {
378
392
  return this.parseJoinQuery<T, any>(select, collection)
379
393
  }
380
-
394
+
381
395
  const query: _storeQuery<T> = {
382
396
  $collection: collection,
383
- $select: select.includes('*') ? undefined : select as Array<keyof T>,
397
+ $select: select.includes('*') ? undefined : (select as Array<keyof T>),
384
398
  $onlyIds: select.includes('_id')
385
399
  }
386
-
400
+
387
401
  if (this.match(TokenType.WHERE)) {
388
402
  query.$ops = this.parseWhereClause<T>()
389
403
  }
390
-
404
+
391
405
  if (this.match(TokenType.GROUP)) {
392
406
  this.advance()
393
407
  this.expect(TokenType.BY)
394
408
  query.$groupby = this.expect(TokenType.IDENTIFIER).value as keyof T
395
409
  }
396
-
410
+
397
411
  if (this.match(TokenType.LIMIT)) {
398
412
  this.advance()
399
413
  query.$limit = parseInt(this.expect(TokenType.NUMBER).value)
400
414
  }
401
-
415
+
402
416
  return query
403
417
  }
404
418
 
405
419
  parseJoinQuery<T extends Record<string, any>, U extends Record<string, any>>(
406
- select: string[],
420
+ select: string[],
407
421
  leftCollection: string
408
422
  ): _join<T, U> {
409
423
  // Parse join type
410
- let joinMode: "inner" | "left" | "right" | "outer" = "inner"
411
-
424
+ let joinMode: 'inner' | 'left' | 'right' | 'outer' = 'inner'
425
+
412
426
  if (this.match(TokenType.INNER)) {
413
427
  this.advance()
414
- joinMode = "inner"
428
+ joinMode = 'inner'
415
429
  } else if (this.match(TokenType.LEFT)) {
416
430
  this.advance()
417
- joinMode = "left"
431
+ joinMode = 'left'
418
432
  } else if (this.match(TokenType.RIGHT)) {
419
433
  this.advance()
420
- joinMode = "right"
434
+ joinMode = 'right'
421
435
  } else if (this.match(TokenType.OUTER)) {
422
436
  this.advance()
423
- joinMode = "outer"
437
+ joinMode = 'outer'
424
438
  }
425
-
439
+
426
440
  this.expect(TokenType.JOIN)
427
441
  const rightCollection = this.expect(TokenType.IDENTIFIER).value
428
442
  this.expect(TokenType.ON)
429
-
443
+
430
444
  // Parse join conditions
431
445
  const onConditions = this.parseJoinConditions<T, U>()
432
-
446
+
433
447
  const joinQuery: _join<T, U> = {
434
448
  $leftCollection: leftCollection,
435
449
  $rightCollection: rightCollection,
436
450
  $mode: joinMode,
437
451
  $on: onConditions,
438
- $select: select.includes('*') ? undefined : select as Array<keyof T | keyof U>
452
+ $select: select.includes('*') ? undefined : (select as Array<keyof T | keyof U>)
439
453
  }
440
-
454
+
441
455
  // Parse additional clauses
442
456
  if (this.match(TokenType.WHERE)) {
443
457
  // For joins, WHERE conditions would need to be handled differently
444
458
  // Skip for now as it's complex with joined tables
445
459
  this.parseWhereClause<T>()
446
460
  }
447
-
461
+
448
462
  if (this.match(TokenType.GROUP)) {
449
463
  this.advance()
450
464
  this.expect(TokenType.BY)
451
465
  joinQuery.$groupby = this.expect(TokenType.IDENTIFIER).value as keyof T | keyof U
452
466
  }
453
-
467
+
454
468
  if (this.match(TokenType.LIMIT)) {
455
469
  this.advance()
456
470
  joinQuery.$limit = parseInt(this.expect(TokenType.NUMBER).value)
457
471
  }
458
-
472
+
459
473
  return joinQuery
460
474
  }
461
475
 
462
476
  private parseJoinConditions<T, U>(): _on<T, U> {
463
477
  const conditions: _on<T, U> = {}
464
-
478
+
465
479
  do {
466
480
  // Parse: table1.column = table2.column
467
481
  const leftSide = this.parseJoinColumn()
468
482
  const operator = this.parseJoinOperator()
469
483
  const rightSide = this.parseJoinColumn()
470
-
484
+
471
485
  // Build the join condition
472
486
  const leftColumn = leftSide.column as keyof T
473
487
  const rightColumn = rightSide.column as keyof U
474
-
488
+
475
489
  if (!conditions[leftColumn]) {
476
490
  conditions[leftColumn] = {} as _joinOperand<U>
477
491
  }
478
-
479
- (conditions[leftColumn] as any)[operator] = rightColumn
480
-
492
+
493
+ ;(conditions[leftColumn] as any)[operator] = rightColumn
494
+
481
495
  if (this.match(TokenType.AND)) {
482
496
  this.advance()
483
497
  } else {
484
498
  break
485
499
  }
486
500
  } while (true)
487
-
501
+
488
502
  return conditions
489
503
  }
490
504
 
491
- private parseJoinColumn(): { table?: string, column: string } {
505
+ private parseJoinColumn(): { table?: string; column: string } {
492
506
  const identifier = this.expect(TokenType.IDENTIFIER).value
493
-
507
+
494
508
  // Check if it's table.column format
495
509
  if (this.current.type === TokenType.IDENTIFIER) {
496
510
  // This might be a qualified column name, but we'll treat it as simple for now
497
511
  return { column: identifier }
498
512
  }
499
-
513
+
500
514
  return { column: identifier }
501
515
  }
502
516
 
@@ -515,7 +529,7 @@ class SQLParser {
515
529
  this.advance()
516
530
  return op
517
531
  }
518
-
532
+
519
533
  throw new Error(`Unknown join operator: ${this.current.type}`)
520
534
  }
521
535
 
@@ -523,7 +537,7 @@ class SQLParser {
523
537
  this.expect(TokenType.INSERT)
524
538
  this.expect(TokenType.INTO)
525
539
  const collection = this.expect(TokenType.IDENTIFIER).value
526
-
540
+
527
541
  // Parse column list
528
542
  let columns: string[] = []
529
543
  if (this.current.type === TokenType.LPAREN) {
@@ -539,28 +553,28 @@ class SQLParser {
539
553
  } while (true)
540
554
  this.expect(TokenType.RPAREN)
541
555
  }
542
-
556
+
543
557
  this.expect(TokenType.VALUES)
544
558
  this.expect(TokenType.LPAREN)
545
-
559
+
546
560
  const values: any = {}
547
561
  let valueIndex = 0
548
-
562
+
549
563
  do {
550
564
  const value = this.parseValue()
551
565
  const column = columns[valueIndex] || `col${valueIndex}`
552
566
  values[column] = value
553
567
  valueIndex++
554
-
568
+
555
569
  if (this.current.type === TokenType.COMMA) {
556
570
  this.advance()
557
571
  } else {
558
572
  break
559
573
  }
560
574
  } while (true)
561
-
575
+
562
576
  this.expect(TokenType.RPAREN)
563
-
577
+
564
578
  return {
565
579
  $collection: collection,
566
580
  $values: values as { [K in keyof T]: T[K] }
@@ -571,27 +585,27 @@ class SQLParser {
571
585
  this.expect(TokenType.UPDATE)
572
586
  const collection = this.expect(TokenType.IDENTIFIER).value
573
587
  this.expect(TokenType.SET)
574
-
588
+
575
589
  const set: any = {}
576
-
590
+
577
591
  do {
578
592
  const column = this.expect(TokenType.IDENTIFIER).value
579
593
  this.expect(TokenType.EQUALS)
580
594
  const value = this.parseValue()
581
595
  set[column] = value
582
-
596
+
583
597
  if (this.current.type === TokenType.COMMA) {
584
598
  this.advance()
585
599
  } else {
586
600
  break
587
601
  }
588
602
  } while (true)
589
-
603
+
590
604
  const update: _storeUpdate<T> = {
591
605
  $collection: collection,
592
606
  $set: set as { [K in keyof Partial<T>]: T[K] }
593
607
  }
594
-
608
+
595
609
  if (this.match(TokenType.WHERE)) {
596
610
  const whereQuery: _storeQuery<T> = {
597
611
  $collection: collection,
@@ -599,7 +613,7 @@ class SQLParser {
599
613
  }
600
614
  update.$where = whereQuery
601
615
  }
602
-
616
+
603
617
  return update
604
618
  }
605
619
 
@@ -607,31 +621,31 @@ class SQLParser {
607
621
  this.expect(TokenType.DELETE)
608
622
  this.expect(TokenType.FROM)
609
623
  const collection = this.expect(TokenType.IDENTIFIER).value
610
-
624
+
611
625
  const deleteQuery: _storeDelete<T> = {
612
626
  $collection: collection
613
627
  }
614
-
628
+
615
629
  if (this.match(TokenType.WHERE)) {
616
630
  deleteQuery.$ops = this.parseWhereClause<T>()
617
631
  }
618
-
632
+
619
633
  return deleteQuery
620
634
  }
621
635
  }
622
636
 
623
637
  // Main SQL to AST converter
624
638
  export class Parser {
625
- static parse<T extends Record<string, any>, U extends Record<string, any> = any>(sql: string):
626
- _storeQuery<T> | _storeInsert<T> | _storeUpdate<T> | _storeDelete<T> | _join<T, U> {
627
-
639
+ static parse<T extends Record<string, any>, U extends Record<string, any> = any>(
640
+ sql: string
641
+ ): _storeQuery<T> | _storeInsert<T> | _storeUpdate<T> | _storeDelete<T> | _join<T, U> {
628
642
  const lexer = new SQLLexer(sql)
629
643
  const tokens = lexer.tokenize()
630
644
  const parser = new SQLParser(tokens)
631
-
645
+
632
646
  // Determine query type based on first token
633
647
  const firstToken = tokens[0]
634
-
648
+
635
649
  switch (firstToken.value) {
636
650
  case TokenType.CREATE:
637
651
  return { $collection: tokens[2].value }
@@ -657,7 +671,7 @@ export class Parser {
657
671
 
658
672
  // Join query builder
659
673
  static join<T extends Record<string, any>, U extends Record<string, any>>(
660
- leftCollection: string,
674
+ leftCollection: string,
661
675
  rightCollection: string
662
676
  ): JoinBuilder<T, U> {
663
677
  return new JoinBuilder<T, U>(leftCollection, rightCollection)
@@ -706,51 +720,58 @@ export class QueryBuilder<T extends Record<string, any>> {
706
720
  // Convert to SQL string (reverse operation)
707
721
  toSQL(): string {
708
722
  let sql = 'SELECT '
709
-
723
+
710
724
  if (this.queryAst.$select) {
711
725
  sql += this.queryAst.$select.join(', ')
712
726
  } else {
713
727
  sql += '*'
714
728
  }
715
-
729
+
716
730
  sql += ` FROM ${this.collection}`
717
-
731
+
718
732
  if (this.queryAst.$ops && this.queryAst.$ops.length > 0) {
719
733
  sql += ' WHERE '
720
- const conditions = this.queryAst.$ops.map(op => {
721
- const entries = Object.entries(op)
722
- return entries.map(([column, operand]) => {
723
- const opEntries = Object.entries(operand as _operand)
724
- return opEntries.map(([operator, value]) => {
725
- const sqlOp = this.operatorToSQL(operator)
726
- const sqlValue = typeof value === 'string' ? `'${value}'` : value
727
- return `${column} ${sqlOp} ${sqlValue}`
728
- }).join(' AND ')
729
- }).join(' AND ')
730
- }).join(' AND ')
734
+ const conditions = this.queryAst.$ops
735
+ .map((op) => {
736
+ const entries = Object.entries(op)
737
+ return entries
738
+ .map(([column, operand]) => {
739
+ const opEntries = Object.entries(operand as _operand)
740
+ return opEntries
741
+ .map(([operator, value]) => {
742
+ const sqlOp = this.operatorToSQL(operator)
743
+ const sqlValue =
744
+ typeof value === 'string' ? `'${value}'` : value
745
+ return `${column} ${sqlOp} ${sqlValue}`
746
+ })
747
+ .join(' AND ')
748
+ })
749
+ .join(' AND ')
750
+ })
751
+ .join(' AND ')
731
752
  sql += conditions
732
753
  }
733
-
754
+
734
755
  if (this.queryAst.$groupby) {
735
756
  sql += ` GROUP BY ${String(this.queryAst.$groupby)}`
736
757
  }
737
-
758
+
738
759
  if (this.queryAst.$limit) {
739
760
  sql += ` LIMIT ${this.queryAst.$limit}`
740
761
  }
741
-
762
+
742
763
  return sql
743
764
  }
744
765
 
745
766
  private operatorToSQL(operator: string): string {
746
767
  const opMap: Record<string, string> = {
747
- '$eq': '=',
748
- '$ne': '!=',
749
- '$gt': '>',
750
- '$lt': '<',
751
- '$gte': '>=',
752
- '$lte': '<=',
753
- '$like': 'LIKE'
768
+ $eq: '=',
769
+ $ne: '!=',
770
+ $gt: '>',
771
+ $lt: '<',
772
+ $gte: '>=',
773
+ $lte: '<=',
774
+ $like: 'LIKE'
754
775
  }
755
776
  return opMap[operator] || '='
756
777
  }
@@ -826,51 +847,55 @@ export class JoinBuilder<T extends Record<string, any>, U extends Record<string,
826
847
  // Convert to SQL string
827
848
  toSQL(): string {
828
849
  let sql = 'SELECT '
829
-
850
+
830
851
  if (this.joinAst.$select) {
831
852
  sql += this.joinAst.$select.join(', ')
832
853
  } else {
833
854
  sql += '*'
834
855
  }
835
-
856
+
836
857
  sql += ` FROM ${this.joinAst.$leftCollection}`
837
-
858
+
838
859
  // Add join type
839
860
  const joinType = this.joinAst.$mode?.toUpperCase() || 'INNER'
840
861
  sql += ` ${joinType} JOIN ${this.joinAst.$rightCollection}`
841
-
862
+
842
863
  // Add ON conditions
843
864
  if (this.joinAst.$on) {
844
865
  sql += ' ON '
845
- const conditions = Object.entries(this.joinAst.$on).map(([leftCol, operand]) => {
846
- return Object.entries(operand as _joinOperand<U>).map(([operator, rightCol]) => {
847
- const sqlOp = this.operatorToSQL(operator)
848
- return `${this.joinAst.$leftCollection}.${leftCol} ${sqlOp} ${this.joinAst.$rightCollection}.${String(rightCol)}`
849
- }).join(' AND ')
850
- }).join(' AND ')
866
+ const conditions = Object.entries(this.joinAst.$on)
867
+ .map(([leftCol, operand]) => {
868
+ return Object.entries(operand as _joinOperand<U>)
869
+ .map(([operator, rightCol]) => {
870
+ const sqlOp = this.operatorToSQL(operator)
871
+ return `${this.joinAst.$leftCollection}.${leftCol} ${sqlOp} ${this.joinAst.$rightCollection}.${String(rightCol)}`
872
+ })
873
+ .join(' AND ')
874
+ })
875
+ .join(' AND ')
851
876
  sql += conditions
852
877
  }
853
-
878
+
854
879
  if (this.joinAst.$groupby) {
855
880
  sql += ` GROUP BY ${String(this.joinAst.$groupby)}`
856
881
  }
857
-
882
+
858
883
  if (this.joinAst.$limit) {
859
884
  sql += ` LIMIT ${this.joinAst.$limit}`
860
885
  }
861
-
886
+
862
887
  return sql
863
888
  }
864
889
 
865
890
  private operatorToSQL(operator: string): string {
866
891
  const opMap: Record<string, string> = {
867
- '$eq': '=',
868
- '$ne': '!=',
869
- '$gt': '>',
870
- '$lt': '<',
871
- '$gte': '>=',
872
- '$lte': '<=',
892
+ $eq: '=',
893
+ $ne: '!=',
894
+ $gt: '>',
895
+ $lt: '<',
896
+ $gte: '>=',
897
+ $lte: '<='
873
898
  }
874
899
  return opMap[operator] || '='
875
900
  }
876
- }
901
+ }