@stravigor/core 0.1.0 → 0.2.0

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 (43) hide show
  1. package/package.json +1 -1
  2. package/src/auth/auth.ts +2 -1
  3. package/src/broadcast/broadcast_manager.ts +18 -5
  4. package/src/broadcast/client.ts +10 -4
  5. package/src/cache/cache_manager.ts +6 -2
  6. package/src/cache/http_cache.ts +1 -5
  7. package/src/core/container.ts +2 -6
  8. package/src/database/database.ts +11 -7
  9. package/src/database/migration/runner.ts +3 -1
  10. package/src/database/query_builder.ts +553 -60
  11. package/src/encryption/encryption_manager.ts +7 -1
  12. package/src/exceptions/errors.ts +1 -5
  13. package/src/exceptions/http_exception.ts +4 -1
  14. package/src/generators/api_generator.ts +8 -1
  15. package/src/generators/doc_generator.ts +33 -28
  16. package/src/generators/model_generator.ts +3 -1
  17. package/src/generators/test_generator.ts +81 -91
  18. package/src/i18n/helpers.ts +5 -1
  19. package/src/i18n/i18n_manager.ts +3 -1
  20. package/src/i18n/middleware.ts +2 -8
  21. package/src/mail/helpers.ts +1 -1
  22. package/src/mail/index.ts +4 -0
  23. package/src/mail/mail_manager.ts +20 -1
  24. package/src/mail/transports/alibaba_transport.ts +88 -0
  25. package/src/mail/transports/mailgun_transport.ts +74 -0
  26. package/src/mail/transports/resend_transport.ts +3 -4
  27. package/src/mail/transports/sendgrid_transport.ts +12 -9
  28. package/src/mail/transports/smtp_transport.ts +5 -5
  29. package/src/mail/types.ts +19 -1
  30. package/src/notification/channels/discord_channel.ts +6 -1
  31. package/src/notification/channels/webhook_channel.ts +8 -3
  32. package/src/notification/helpers.ts +7 -7
  33. package/src/notification/notification_manager.ts +7 -6
  34. package/src/orm/base_model.ts +4 -2
  35. package/src/queue/queue.ts +3 -1
  36. package/src/scheduler/cron.ts +12 -6
  37. package/src/scheduler/schedule.ts +17 -8
  38. package/src/session/session_manager.ts +3 -1
  39. package/src/storage/storage_manager.ts +3 -1
  40. package/src/view/compiler.ts +1 -3
  41. package/src/view/islands/island_builder.ts +4 -4
  42. package/src/view/islands/vue_plugin.ts +11 -15
  43. package/src/view/tokenizer.ts +11 -1
@@ -89,9 +89,7 @@ export default class TestGenerator {
89
89
  const routesImport = relativeImport(this.paths.tests, this.paths.routes)
90
90
  const userSchema = this.findUserEntity()
91
91
  const userPkName = userSchema ? this.findSchemaPK(userSchema.name) : 'id'
92
- const userPkIsUuid = userSchema
93
- ? this.getPkType(userSchema) === 'uuid'
94
- : true
92
+ const userPkIsUuid = userSchema ? this.getPkType(userSchema) === 'uuid' : true
95
93
 
96
94
  const lines: string[] = [
97
95
  '// Generated by Strav — DO NOT EDIT',
@@ -129,7 +127,9 @@ export default class TestGenerator {
129
127
  if (userSchema) {
130
128
  const modelImport = relativeImport(this.paths.tests, this.paths.models)
131
129
  const modelFile = toSnakeCase(userSchema.name)
132
- lines.push(` const { default: ${toPascalCase(userSchema.name)} } = await import('${modelImport}/${modelFile}')`)
130
+ lines.push(
131
+ ` const { default: ${toPascalCase(userSchema.name)} } = await import('${modelImport}/${modelFile}')`
132
+ )
133
133
  lines.push(` return ${toPascalCase(userSchema.name)}.find(id as string)`)
134
134
  } else {
135
135
  lines.push(` return null`)
@@ -210,7 +210,7 @@ export default class TestGenerator {
210
210
  }
211
211
 
212
212
  lines.push('')
213
- lines.push(' const result = await AccessToken.create(testUserPid, \'test-token\')')
213
+ lines.push(" const result = await AccessToken.create(testUserPid, 'test-token')")
214
214
  lines.push(' token = result.token')
215
215
  lines.push('}')
216
216
  lines.push('')
@@ -416,7 +416,7 @@ export default class TestGenerator {
416
416
  ` const response = routerInstance.handle(`,
417
417
  ` new Request('${this.requestBaseUrl()}${testPath}', {`,
418
418
  ` headers: {`,
419
- ` Authorization: 'NotBearer some_token',`,
419
+ ` Authorization: 'NotBearer some_token',`
420
420
  )
421
421
 
422
422
  if (this.apiConfig.routing === ApiRouting.Subdomain) {
@@ -431,7 +431,7 @@ export default class TestGenerator {
431
431
  ` expect(res.status).toBe(401)`,
432
432
  ` })`,
433
433
  '})',
434
- '',
434
+ ''
435
435
  )
436
436
 
437
437
  return {
@@ -525,7 +525,7 @@ export default class TestGenerator {
525
525
  ` const res = await request('GET', \`${routePath}/\${createdId}\`)`,
526
526
  ` expect(res.status).toBe(200)`,
527
527
  ` })`,
528
- '',
528
+ ''
529
529
  )
530
530
 
531
531
  if (Object.keys(updatePayload).length > 0) {
@@ -534,7 +534,7 @@ export default class TestGenerator {
534
534
  ` const res = await request('PUT', \`${routePath}/\${createdId}\`, ${updatePayload})`,
535
535
  ` expect(res.status).toBe(200)`,
536
536
  ` })`,
537
- '',
537
+ ''
538
538
  )
539
539
  }
540
540
 
@@ -550,7 +550,7 @@ export default class TestGenerator {
550
550
  ` const res = await request('GET', \`${routePath}/\${createdId}\`)`,
551
551
  ` expect(res.status).toBe(404)`,
552
552
  ` })`,
553
- '',
553
+ ''
554
554
  )
555
555
 
556
556
  // Validation test
@@ -562,7 +562,7 @@ export default class TestGenerator {
562
562
  ` const body = (await res.json()) as any`,
563
563
  ` expect(body.errors).toBeDefined()`,
564
564
  ` })`,
565
- '',
565
+ ''
566
566
  )
567
567
  }
568
568
 
@@ -572,7 +572,7 @@ export default class TestGenerator {
572
572
  ` expect(res.status).toBe(404)`,
573
573
  ` })`,
574
574
  '})',
575
- '',
575
+ ''
576
576
  )
577
577
 
578
578
  return {
@@ -585,7 +585,10 @@ export default class TestGenerator {
585
585
  // Contribution tests (dependent, full CRUD + soft delete)
586
586
  // ---------------------------------------------------------------------------
587
587
 
588
- private generateContributionTest(schema: SchemaDefinition, table: TableDefinition): GeneratedFile {
588
+ private generateContributionTest(
589
+ schema: SchemaDefinition,
590
+ table: TableDefinition
591
+ ): GeneratedFile {
589
592
  const snakeName = toSnakeCase(schema.name)
590
593
  const displayName = snakeName.replace(/_/g, ' ')
591
594
  const parentName = schema.parent!
@@ -609,18 +612,16 @@ export default class TestGenerator {
609
612
  }
610
613
  if (parentChain.some(p => !p.isTestUser)) lines.push('')
611
614
 
612
- lines.push(
613
- 'beforeAll(async () => {',
614
- ' await cleanup()',
615
- ' await createTestUser()',
616
- )
615
+ lines.push('beforeAll(async () => {', ' await cleanup()', ' await createTestUser()')
617
616
 
618
617
  // Create parent chain
619
618
  for (const p of parentChain) {
620
619
  if (p.isTestUser) continue
621
620
  lines.push('')
622
621
  lines.push(` // Create ${p.schemaName} to serve as parent`)
623
- lines.push(` const ${p.varName}Res = await request('POST', \`${p.createPath}\`, ${p.createPayload})`)
622
+ lines.push(
623
+ ` const ${p.varName}Res = await request('POST', \`${p.createPath}\`, ${p.createPayload})`
624
+ )
624
625
  lines.push(` const ${p.varName}Data = (await ${p.varName}Res.json()) as any`)
625
626
  lines.push(` ${p.varName} = ${p.varName}Data.${p.pkField}`)
626
627
  }
@@ -634,7 +635,7 @@ export default class TestGenerator {
634
635
  '',
635
636
  `describe('contribution archetype: ${snakeName} (full CRUD${hasSoftDelete ? ' + soft delete' : ''}, parent: ${parentName})', () => {`,
636
637
  ` let createdId: string | number`,
637
- '',
638
+ ''
638
639
  )
639
640
 
640
641
  const testRoutePath = this.buildTestRoutePath(schema, parentChain)
@@ -643,7 +644,7 @@ export default class TestGenerator {
643
644
  ` test('POST ${routePath} → 201 (create with parent FK)', async () => {`,
644
645
  ` const res = await request('POST', \`${testRoutePath}\`, ${storePayload})`,
645
646
  ` expect(res.status).toBe(201)`,
646
- ` const data = (await res.json()) as any`,
647
+ ` const data = (await res.json()) as any`
647
648
  )
648
649
  const firstField = this.getFirstFieldForAssertion(schema)
649
650
  if (firstField) {
@@ -666,7 +667,7 @@ export default class TestGenerator {
666
667
  ` const res = await request('GET', \`${testRoutePath}/\${createdId}\`)`,
667
668
  ` expect(res.status).toBe(200)`,
668
669
  ` })`,
669
- '',
670
+ ''
670
671
  )
671
672
 
672
673
  if (Object.keys(updatePayload).length > 0) {
@@ -675,7 +676,7 @@ export default class TestGenerator {
675
676
  ` const res = await request('PUT', \`${testRoutePath}/\${createdId}\`, ${updatePayload})`,
676
677
  ` expect(res.status).toBe(200)`,
677
678
  ` })`,
678
- '',
679
+ ''
679
680
  )
680
681
  }
681
682
 
@@ -692,7 +693,7 @@ export default class TestGenerator {
692
693
  ` expect(res.status).toBe(404)`,
693
694
  ` })`,
694
695
  '})',
695
- '',
696
+ ''
696
697
  )
697
698
 
698
699
  return {
@@ -756,7 +757,7 @@ export default class TestGenerator {
756
757
  ` const res = await request('GET', \`${routePath}/\${createdId}\`)`,
757
758
  ` expect(res.status).toBe(200)`,
758
759
  ` })`,
759
- '',
760
+ ''
760
761
  )
761
762
 
762
763
  if (Object.keys(updatePayload).length > 0) {
@@ -765,7 +766,7 @@ export default class TestGenerator {
765
766
  ` const res = await request('PUT', \`${routePath}/\${createdId}\`, ${updatePayload})`,
766
767
  ` expect(res.status).toBe(200)`,
767
768
  ` })`,
768
- '',
769
+ ''
769
770
  )
770
771
  }
771
772
 
@@ -782,7 +783,7 @@ export default class TestGenerator {
782
783
  ` expect(res.status).toBe(404)`,
783
784
  ` })`,
784
785
  '})',
785
- '',
786
+ ''
786
787
  )
787
788
 
788
789
  return {
@@ -816,17 +817,15 @@ export default class TestGenerator {
816
817
  }
817
818
  if (parentChain.some(p => !p.isTestUser)) lines.push('')
818
819
 
819
- lines.push(
820
- 'beforeAll(async () => {',
821
- ' await cleanup()',
822
- ' await createTestUser()',
823
- )
820
+ lines.push('beforeAll(async () => {', ' await cleanup()', ' await createTestUser()')
824
821
 
825
822
  for (const p of parentChain) {
826
823
  if (p.isTestUser) continue
827
824
  lines.push('')
828
825
  lines.push(` // Create a ${p.schemaName} to serve as parent`)
829
- lines.push(` const ${p.varName}Res = await request('POST', \`${p.createPath}\`, ${p.createPayload})`)
826
+ lines.push(
827
+ ` const ${p.varName}Res = await request('POST', \`${p.createPath}\`, ${p.createPayload})`
828
+ )
830
829
  lines.push(` const ${p.varName}Data = (await ${p.varName}Res.json()) as any`)
831
830
  lines.push(` ${p.varName} = ${p.varName}Data.${p.pkField}`)
832
831
  }
@@ -840,7 +839,7 @@ export default class TestGenerator {
840
839
  '',
841
840
  `describe('attribute archetype: ${snakeName} (full CRUD, parent: ${parentName})', () => {`,
842
841
  ` let createdId: string | number`,
843
- '',
842
+ ''
844
843
  )
845
844
 
846
845
  const testRoutePath = this.buildTestRoutePath(schema, parentChain)
@@ -866,7 +865,7 @@ export default class TestGenerator {
866
865
  ` const res = await request('GET', \`${testRoutePath}/\${createdId}\`)`,
867
866
  ` expect(res.status).toBe(200)`,
868
867
  ` })`,
869
- '',
868
+ ''
870
869
  )
871
870
 
872
871
  if (Object.keys(updatePayload).length > 0) {
@@ -875,7 +874,7 @@ export default class TestGenerator {
875
874
  ` const res = await request('PUT', \`${testRoutePath}/\${createdId}\`, ${updatePayload})`,
876
875
  ` expect(res.status).toBe(200)`,
877
876
  ` })`,
878
- '',
877
+ ''
879
878
  )
880
879
  }
881
880
 
@@ -892,7 +891,7 @@ export default class TestGenerator {
892
891
  ` expect(res.status).toBe(404)`,
893
892
  ` })`,
894
893
  '})',
895
- '',
894
+ ''
896
895
  )
897
896
 
898
897
  return {
@@ -929,17 +928,15 @@ export default class TestGenerator {
929
928
  lines.push('let contentId: string | number')
930
929
  lines.push('')
931
930
 
932
- lines.push(
933
- 'beforeAll(async () => {',
934
- ' await cleanup()',
935
- ' await createTestUser()',
936
- )
931
+ lines.push('beforeAll(async () => {', ' await cleanup()', ' await createTestUser()')
937
932
 
938
933
  for (const p of parentChain) {
939
934
  if (p.isTestUser) continue
940
935
  lines.push('')
941
936
  lines.push(` // Create a ${p.schemaName} to serve as parent`)
942
- lines.push(` const ${p.varName}Res = await request('POST', \`${p.createPath}\`, ${p.createPayload})`)
937
+ lines.push(
938
+ ` const ${p.varName}Res = await request('POST', \`${p.createPath}\`, ${p.createPayload})`
939
+ )
943
940
  lines.push(` const ${p.varName}Data = (await ${p.varName}Res.json()) as any`)
944
941
  lines.push(` ${p.varName} = ${p.varName}Data.${p.pkField}`)
945
942
  }
@@ -963,7 +960,7 @@ export default class TestGenerator {
963
960
  '})',
964
961
  '',
965
962
  `describe('component archetype: ${snakeName} (index, show, update only)', () => {`,
966
- '',
963
+ ''
967
964
  )
968
965
 
969
966
  const testRoutePath = this.buildTestRoutePath(schema, parentChain)
@@ -981,7 +978,7 @@ export default class TestGenerator {
981
978
  ` const res = await request('GET', \`${testRoutePath}/\${contentId}\`)`,
982
979
  ` expect(res.status).toBe(200)`,
983
980
  ` })`,
984
- '',
981
+ ''
985
982
  )
986
983
 
987
984
  if (Object.keys(updatePayload).length > 0) {
@@ -990,7 +987,7 @@ export default class TestGenerator {
990
987
  ` const res = await request('PUT', \`${testRoutePath}/\${contentId}\`, ${updatePayload})`,
991
988
  ` expect(res.status).toBe(200)`,
992
989
  ` })`,
993
- '',
990
+ ''
994
991
  )
995
992
  }
996
993
 
@@ -1005,7 +1002,7 @@ export default class TestGenerator {
1005
1002
  ` expect(res.status).toBe(404)`,
1006
1003
  ` })`,
1007
1004
  '})',
1008
- '',
1005
+ ''
1009
1006
  )
1010
1007
 
1011
1008
  return {
@@ -1038,17 +1035,15 @@ export default class TestGenerator {
1038
1035
  }
1039
1036
  if (parentChain.some(p => !p.isTestUser)) lines.push('')
1040
1037
 
1041
- lines.push(
1042
- 'beforeAll(async () => {',
1043
- ' await cleanup()',
1044
- ' await createTestUser()',
1045
- )
1038
+ lines.push('beforeAll(async () => {', ' await cleanup()', ' await createTestUser()')
1046
1039
 
1047
1040
  for (const p of parentChain) {
1048
1041
  if (p.isTestUser) continue
1049
1042
  lines.push('')
1050
1043
  lines.push(` // Create a ${p.schemaName} to serve as parent`)
1051
- lines.push(` const ${p.varName}Res = await request('POST', \`${p.createPath}\`, ${p.createPayload})`)
1044
+ lines.push(
1045
+ ` const ${p.varName}Res = await request('POST', \`${p.createPath}\`, ${p.createPayload})`
1046
+ )
1052
1047
  lines.push(` const ${p.varName}Data = (await ${p.varName}Res.json()) as any`)
1053
1048
  lines.push(` ${p.varName} = ${p.varName}Data.${p.pkField}`)
1054
1049
  }
@@ -1062,7 +1057,7 @@ export default class TestGenerator {
1062
1057
  '',
1063
1058
  `describe('event archetype: ${snakeName} (index, show, store — append only)', () => {`,
1064
1059
  ` let createdId: string | number`,
1065
- '',
1060
+ ''
1066
1061
  )
1067
1062
 
1068
1063
  const testRoutePath = this.buildTestRoutePath(schema, parentChain)
@@ -1099,7 +1094,7 @@ export default class TestGenerator {
1099
1094
  ` expect(res.status).toBe(404)`,
1100
1095
  ` })`,
1101
1096
  '})',
1102
- '',
1097
+ ''
1103
1098
  )
1104
1099
 
1105
1100
  return {
@@ -1112,7 +1107,10 @@ export default class TestGenerator {
1112
1107
  // Configuration tests (dependent, singleton — show/upsert/reset)
1113
1108
  // ---------------------------------------------------------------------------
1114
1109
 
1115
- private generateConfigurationTest(schema: SchemaDefinition, table: TableDefinition): GeneratedFile {
1110
+ private generateConfigurationTest(
1111
+ schema: SchemaDefinition,
1112
+ table: TableDefinition
1113
+ ): GeneratedFile {
1116
1114
  const snakeName = toSnakeCase(schema.name)
1117
1115
  const parentName = schema.parent!
1118
1116
  const routePath = this.buildRoutePath(schema)
@@ -1133,17 +1131,15 @@ export default class TestGenerator {
1133
1131
  }
1134
1132
  if (parentChain.some(p => !p.isTestUser)) lines.push('')
1135
1133
 
1136
- lines.push(
1137
- 'beforeAll(async () => {',
1138
- ' await cleanup()',
1139
- ' await createTestUser()',
1140
- )
1134
+ lines.push('beforeAll(async () => {', ' await cleanup()', ' await createTestUser()')
1141
1135
 
1142
1136
  for (const p of parentChain) {
1143
1137
  if (p.isTestUser) continue
1144
1138
  lines.push('')
1145
1139
  lines.push(` // Create a ${p.schemaName} to serve as parent`)
1146
- lines.push(` const ${p.varName}Res = await request('POST', \`${p.createPath}\`, ${p.createPayload})`)
1140
+ lines.push(
1141
+ ` const ${p.varName}Res = await request('POST', \`${p.createPath}\`, ${p.createPayload})`
1142
+ )
1147
1143
  lines.push(` const ${p.varName}Data = (await ${p.varName}Res.json()) as any`)
1148
1144
  lines.push(` ${p.varName} = ${p.varName}Data.${p.pkField}`)
1149
1145
  }
@@ -1156,7 +1152,7 @@ export default class TestGenerator {
1156
1152
  '})',
1157
1153
  '',
1158
1154
  `describe('configuration archetype: ${snakeName} (show, upsert, reset — singleton)', () => {`,
1159
- '',
1155
+ ''
1160
1156
  )
1161
1157
 
1162
1158
  const testRoutePath = this.buildTestRoutePath(schema, parentChain)
@@ -1176,7 +1172,7 @@ export default class TestGenerator {
1176
1172
  ` const res = await request('GET', \`${testRoutePath}\`)`,
1177
1173
  ` expect(res.status).toBe(200)`,
1178
1174
  ` })`,
1179
- '',
1175
+ ''
1180
1176
  )
1181
1177
 
1182
1178
  if (Object.keys(updatePayload).length > 0) {
@@ -1185,7 +1181,7 @@ export default class TestGenerator {
1185
1181
  ` const res = await request('PUT', \`${testRoutePath}\`, ${updatePayload})`,
1186
1182
  ` expect(res.status).toBe(200)`,
1187
1183
  ` })`,
1188
- '',
1184
+ ''
1189
1185
  )
1190
1186
  }
1191
1187
 
@@ -1202,7 +1198,7 @@ export default class TestGenerator {
1202
1198
  ` expect(res.status).toBe(404)`,
1203
1199
  ` })`,
1204
1200
  '})',
1205
- '',
1201
+ ''
1206
1202
  )
1207
1203
 
1208
1204
  return {
@@ -1231,10 +1227,7 @@ export default class TestGenerator {
1231
1227
  }
1232
1228
 
1233
1229
  /** Build the route path with template literal variables for actual test requests. */
1234
- private buildTestRoutePath(
1235
- schema: SchemaDefinition,
1236
- parentChain: ParentChainEntry[]
1237
- ): string {
1230
+ private buildTestRoutePath(schema: SchemaDefinition, parentChain: ParentChainEntry[]): string {
1238
1231
  const isDependent = DEPENDENT_ARCHETYPES.has(schema.archetype)
1239
1232
  const prefix = this.apiConfig.routing === ApiRouting.Prefix ? this.apiConfig.prefix : ''
1240
1233
 
@@ -1262,9 +1255,7 @@ export default class TestGenerator {
1262
1255
 
1263
1256
  /** Find the user entity schema (used for test user creation). */
1264
1257
  private findUserEntity(): SchemaDefinition | undefined {
1265
- return this.schemas.find(
1266
- s => s.archetype === Archetype.Entity && s.name === 'user'
1267
- )
1258
+ return this.schemas.find(s => s.archetype === Archetype.Entity && s.name === 'user')
1268
1259
  }
1269
1260
 
1270
1261
  /** Find the primary key field name for a schema. */
@@ -1298,9 +1289,7 @@ export default class TestGenerator {
1298
1289
  }
1299
1290
 
1300
1291
  /** Find the first required non-PK field. */
1301
- private findFirstRequiredField(
1302
- schema: SchemaDefinition
1303
- ): { name: string; camel: string } | null {
1292
+ private findFirstRequiredField(schema: SchemaDefinition): { name: string; camel: string } | null {
1304
1293
  for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
1305
1294
  if (fieldDef.primaryKey) continue
1306
1295
  if (SYSTEM_COLUMNS.has(toSnakeCase(fieldName))) continue
@@ -1346,7 +1335,9 @@ export default class TestGenerator {
1346
1335
  if (fieldDef.sensitive) {
1347
1336
  return 'hashed_fixture'
1348
1337
  }
1349
- const label = toPascalCase(fieldName).replace(/([A-Z])/g, ' $1').trim()
1338
+ const label = toPascalCase(fieldName)
1339
+ .replace(/([A-Z])/g, ' $1')
1340
+ .trim()
1350
1341
  return `Fixture ${label}`
1351
1342
  }
1352
1343
 
@@ -1420,17 +1411,16 @@ export default class TestGenerator {
1420
1411
  case 'character':
1421
1412
  case 'text':
1422
1413
  default: {
1423
- const label = toPascalCase(fieldName).replace(/([A-Z])/g, ' $1').trim()
1414
+ const label = toPascalCase(fieldName)
1415
+ .replace(/([A-Z])/g, ' $1')
1416
+ .trim()
1424
1417
  return mode === 'js' ? `'Test ${label}'` : `Test ${label}`
1425
1418
  }
1426
1419
  }
1427
1420
  }
1428
1421
 
1429
1422
  /** Generate an updated sample value (different from store). */
1430
- private sampleUpdateValue(
1431
- fieldName: string,
1432
- fieldDef: FieldDefinition
1433
- ): string {
1423
+ private sampleUpdateValue(fieldName: string, fieldDef: FieldDefinition): string {
1434
1424
  if (this.isEmailField(fieldName, fieldDef)) {
1435
1425
  return `'updated-${toSnakeCase(fieldName)}@example.com'`
1436
1426
  }
@@ -1464,7 +1454,9 @@ export default class TestGenerator {
1464
1454
  case 'money':
1465
1455
  return '19.99'
1466
1456
  default: {
1467
- const label = toPascalCase(fieldName).replace(/([A-Z])/g, ' $1').trim()
1457
+ const label = toPascalCase(fieldName)
1458
+ .replace(/([A-Z])/g, ' $1')
1459
+ .trim()
1468
1460
  return `'Updated ${label}'`
1469
1461
  }
1470
1462
  }
@@ -1556,7 +1548,8 @@ export default class TestGenerator {
1556
1548
  const parentSchema = this.schemaMap.get(current)
1557
1549
  if (!parentSchema) break
1558
1550
 
1559
- const isUserEntity = parentSchema.archetype === Archetype.Entity && parentSchema.name === 'user'
1551
+ const isUserEntity =
1552
+ parentSchema.archetype === Archetype.Entity && parentSchema.name === 'user'
1560
1553
  const pkName = this.findSchemaPK(parentSchema.name)
1561
1554
 
1562
1555
  chain.unshift({
@@ -1582,7 +1575,9 @@ export default class TestGenerator {
1582
1575
  // Build the path to create this parent
1583
1576
  if (DEPENDENT_ARCHETYPES.has(parentSchema.archetype) && parentSchema.parent) {
1584
1577
  const grandParent = chain[i - 1]
1585
- const grandParentVar = grandParent?.isTestUser ? 'testUserPid' : grandParent?.varName ?? 'testUserPid'
1578
+ const grandParentVar = grandParent?.isTestUser
1579
+ ? 'testUserPid'
1580
+ : (grandParent?.varName ?? 'testUserPid')
1586
1581
  const parentSegment = toRouteSegment(parentSchema.parent)
1587
1582
  const childSegment = toChildSegment(parentSchema.name, parentSchema.parent)
1588
1583
  const prefix = this.apiConfig.routing === ApiRouting.Prefix ? this.apiConfig.prefix : ''
@@ -1631,12 +1626,7 @@ export default class TestGenerator {
1631
1626
  }
1632
1627
  dependents.sort((a, b) => (depthMap.get(b) ?? 0) - (depthMap.get(a) ?? 0))
1633
1628
 
1634
- return [
1635
- ...associations,
1636
- ...dependents,
1637
- '_strav_access_tokens',
1638
- ...roots,
1639
- ]
1629
+ return [...associations, ...dependents, '_strav_access_tokens', ...roots]
1640
1630
  }
1641
1631
 
1642
1632
  /** Compute the nesting depth of a schema (0 for root, 1 for direct child, etc.). */
@@ -65,7 +65,11 @@ export function t(key: string, replacements?: Record<string, string | number>):
65
65
  * choice('messages.apple', 1) // "one apple"
66
66
  * choice('messages.apple', 5) // "5 apples"
67
67
  */
68
- export function choice(key: string, count: number, replacements?: Record<string, string | number>): string {
68
+ export function choice(
69
+ key: string,
70
+ count: number,
71
+ replacements?: Record<string, string | number>
72
+ ): string {
69
73
  if (!I18nManager.isLoaded) return key
70
74
 
71
75
  return translateChoice(locale(), key, count, replacements)
@@ -38,7 +38,9 @@ export default class I18nManager {
38
38
 
39
39
  static get config(): I18nConfig {
40
40
  if (!I18nManager._config) {
41
- throw new ConfigurationError('I18nManager not configured. Resolve it through the container first.')
41
+ throw new ConfigurationError(
42
+ 'I18nManager not configured. Resolve it through the container first.'
43
+ )
42
44
  }
43
45
  return I18nManager._config
44
46
  }
@@ -42,10 +42,7 @@ function detectLocale(ctx: Context): string {
42
42
  break
43
43
  }
44
44
  case 'header': {
45
- const match = parseAcceptLanguage(
46
- ctx.headers.get('accept-language'),
47
- supported
48
- )
45
+ const match = parseAcceptLanguage(ctx.headers.get('accept-language'), supported)
49
46
  if (match) return match
50
47
  break
51
48
  }
@@ -62,10 +59,7 @@ function detectLocale(ctx: Context): string {
62
59
  * @example
63
60
  * parseAcceptLanguage('fr-FR,fr;q=0.9,en;q=0.8', ['en', 'fr']) // 'fr'
64
61
  */
65
- export function parseAcceptLanguage(
66
- header: string | null,
67
- supported: string[]
68
- ): string | null {
62
+ export function parseAcceptLanguage(header: string | null, supported: string[]): string | null {
69
63
  if (!header) return null
70
64
 
71
65
  // Parse entries like "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7"
@@ -205,7 +205,7 @@ export const mail = {
205
205
  * Call this in your app bootstrap after Queue is configured.
206
206
  */
207
207
  registerQueueHandler(): void {
208
- Queue.handle<MailMessage>('strav:send-mail', async (message) => {
208
+ Queue.handle<MailMessage>('strav:send-mail', async message => {
209
209
  await MailManager.transport.send(message)
210
210
  })
211
211
  },
package/src/mail/index.ts CHANGED
@@ -3,6 +3,8 @@ export { mail, PendingMail } from './helpers.ts'
3
3
  export { SmtpTransport } from './transports/smtp_transport.ts'
4
4
  export { ResendTransport } from './transports/resend_transport.ts'
5
5
  export { SendGridTransport } from './transports/sendgrid_transport.ts'
6
+ export { MailgunTransport } from './transports/mailgun_transport.ts'
7
+ export { AlibabaTransport } from './transports/alibaba_transport.ts'
6
8
  export { LogTransport } from './transports/log_transport.ts'
7
9
  export { inlineCss } from './css_inliner.ts'
8
10
  export type {
@@ -14,6 +16,8 @@ export type {
14
16
  SmtpConfig,
15
17
  ResendConfig,
16
18
  SendGridConfig,
19
+ MailgunConfig,
20
+ AlibabaConfig,
17
21
  LogConfig,
18
22
  } from './types.ts'
19
23
  export type { InlinerOptions } from './css_inliner.ts'
@@ -4,6 +4,8 @@ import Configuration from '../config/configuration.ts'
4
4
  import { SmtpTransport } from './transports/smtp_transport.ts'
5
5
  import { ResendTransport } from './transports/resend_transport.ts'
6
6
  import { SendGridTransport } from './transports/sendgrid_transport.ts'
7
+ import { MailgunTransport } from './transports/mailgun_transport.ts'
8
+ import { AlibabaTransport } from './transports/alibaba_transport.ts'
7
9
  import { LogTransport } from './transports/log_transport.ts'
8
10
  import type { MailTransport, MailConfig } from './types.ts'
9
11
 
@@ -48,6 +50,17 @@ export default class MailManager {
48
50
  apiKey: '',
49
51
  ...(config.get('mail.sendgrid', {}) as object),
50
52
  },
53
+ mailgun: {
54
+ apiKey: '',
55
+ domain: '',
56
+ ...(config.get('mail.mailgun', {}) as object),
57
+ },
58
+ alibaba: {
59
+ accessKeyId: '',
60
+ accessKeySecret: '',
61
+ accountName: '',
62
+ ...(config.get('mail.alibaba', {}) as object),
63
+ },
51
64
  log: {
52
65
  output: 'console',
53
66
  ...(config.get('mail.log', {}) as object),
@@ -65,6 +78,10 @@ export default class MailManager {
65
78
  return new ResendTransport(MailManager._config.resend)
66
79
  case 'sendgrid':
67
80
  return new SendGridTransport(MailManager._config.sendgrid)
81
+ case 'mailgun':
82
+ return new MailgunTransport(MailManager._config.mailgun)
83
+ case 'alibaba':
84
+ return new AlibabaTransport(MailManager._config.alibaba)
68
85
  case 'log':
69
86
  return new LogTransport(MailManager._config.log)
70
87
  default:
@@ -76,7 +93,9 @@ export default class MailManager {
76
93
 
77
94
  static get transport(): MailTransport {
78
95
  if (!MailManager._transport) {
79
- throw new ConfigurationError('MailManager not configured. Resolve it through the container first.')
96
+ throw new ConfigurationError(
97
+ 'MailManager not configured. Resolve it through the container first.'
98
+ )
80
99
  }
81
100
  return MailManager._transport
82
101
  }