@deterministic-code/datasources 0.1.0 → 0.1.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 (302) hide show
  1. package/dist/ArrayReader.d.ts +12 -0
  2. package/dist/BatchCommandIBulkWriteStrategy.d.ts +33 -0
  3. package/dist/BuiltRepository.d.ts +16 -0
  4. package/dist/BulkCRUDRepository.d.ts +17 -0
  5. package/dist/BulkCommand.d.ts +5 -0
  6. package/dist/BulkWriteEnrichment.d.ts +4 -0
  7. package/dist/CRUDRepository.d.ts +40 -0
  8. package/dist/CRUDRepositoryBuilder.d.ts +19 -0
  9. package/dist/CRUDRepositoryOptions.d.ts +7 -0
  10. package/dist/CRUDRepositoryStatements.d.ts +17 -0
  11. package/dist/CascadeDeleteEnrichmentStrategy.d.ts +7 -0
  12. package/dist/CascadeDeleteUpdateEnrichmentStrategy.d.ts +8 -0
  13. package/dist/CommandMiddlewareHost.d.ts +22 -0
  14. package/dist/ConsoleCommandMiddleware.d.ts +14 -0
  15. package/dist/CreateEnrichmentStrategy.d.ts +14 -0
  16. package/dist/Datasource.d.ts +30 -0
  17. package/dist/DatasourceError.d.ts +3 -0
  18. package/dist/DatasourceFactory.d.ts +12 -0
  19. package/dist/DatasourceOptions.d.ts +1 -0
  20. package/dist/DatasourceUrl.d.ts +12 -0
  21. package/dist/DatasourceUrlParser.d.ts +4 -0
  22. package/dist/DefaultCreateEnrichmentStrategy.d.ts +7 -0
  23. package/dist/DefaultReadonlyEnrichmentStrategy.d.ts +19 -0
  24. package/dist/DefaultUpdateEnrichmentStrategy.d.ts +7 -0
  25. package/dist/DefaultWriteEnrichmentStrategy.d.ts +21 -0
  26. package/dist/DeleteEnrichmentStrategy.d.ts +11 -0
  27. package/dist/DriverLoader.d.ts +5 -0
  28. package/dist/Enrichment.d.ts +13 -0
  29. package/dist/EnrichmentPropertyType.d.ts +7 -0
  30. package/dist/ExpectConsoleMiddleware.d.ts +15 -0
  31. package/dist/FirstResultReader.d.ts +19 -0
  32. package/dist/HashsetOptimizedReadonlyEnrichmentStrategy.d.ts +9 -0
  33. package/dist/IBulkWriteStrategy.d.ts +12 -0
  34. package/dist/ICRUDRepository.d.ts +12 -0
  35. package/dist/ICRUDRepositoryFactory.d.ts +7 -0
  36. package/dist/ICommand.d.ts +20 -0
  37. package/dist/ICommandMiddleware.d.ts +12 -0
  38. package/dist/IConnection.d.ts +12 -0
  39. package/dist/IDatasource.d.ts +18 -0
  40. package/dist/IDatasourceFactory.d.ts +5 -0
  41. package/dist/IDriver.d.ts +19 -0
  42. package/dist/IEnriched.d.ts +6 -0
  43. package/dist/IReader.d.ts +8 -0
  44. package/dist/IReadonlyEnrichmentStrategy.d.ts +7 -0
  45. package/dist/IReadonlyRepository.d.ts +11 -0
  46. package/dist/IRepository.d.ts +4 -0
  47. package/dist/IRepositoryFactory.d.ts +7 -0
  48. package/dist/IRepositoryTree.d.ts +4 -0
  49. package/dist/IRepositoryTreeNode.d.ts +7 -0
  50. package/dist/ITransaction.d.ts +8 -0
  51. package/dist/IUnitOfWork.d.ts +5 -0
  52. package/dist/IWriteEnrichmentStrategy.d.ts +7 -0
  53. package/dist/Information.d.ts +5 -0
  54. package/dist/IsolationLevel.d.ts +7 -0
  55. package/dist/LazyWriteEnrichmentStrategy.d.ts +4 -0
  56. package/dist/NestedToObjectEnrichmentStrategy.d.ts +10 -0
  57. package/dist/Parameter.d.ts +7 -0
  58. package/dist/ParameterDirection.d.ts +7 -0
  59. package/dist/ProviderCatalog.d.ts +7 -0
  60. package/dist/ProviderNotImplementedError.d.ts +4 -0
  61. package/dist/ProviderNotInstalledError.d.ts +4 -0
  62. package/dist/ReadonlyRepository.d.ts +59 -0
  63. package/dist/ReadonlyRepositoryBuilder.d.ts +12 -0
  64. package/dist/ReadonlyRepositoryMappingOptions.d.ts +14 -0
  65. package/dist/ReadonlyRepositoryStatements.d.ts +8 -0
  66. package/dist/RepositoryAccess.d.ts +11 -0
  67. package/dist/RepositoryFactory.d.ts +23 -0
  68. package/dist/RepositoryNotRegisteredError.d.ts +4 -0
  69. package/dist/RepositoryTree.d.ts +6 -0
  70. package/dist/RepositoryTreeNode.d.ts +9 -0
  71. package/dist/Response.d.ts +7 -0
  72. package/dist/Row.d.ts +2 -0
  73. package/dist/SingleCommandIBulkWriteStrategy.d.ts +11 -0
  74. package/dist/StatementMappers.d.ts +16 -0
  75. package/dist/StoredEnrichment.d.ts +11 -0
  76. package/dist/StoredProcedureBulkWriteStrategy.d.ts +27 -0
  77. package/dist/StreamingReader.d.ts +16 -0
  78. package/dist/UnitOfWorkTransaction.d.ts +12 -0
  79. package/dist/UpdateEnrichmentStrategy.d.ts +18 -0
  80. package/dist/WriteMode.d.ts +1 -0
  81. package/dist/builders/Field.d.ts +5 -0
  82. package/dist/builders/Scalar.d.ts +1 -0
  83. package/dist/builders/index.d.ts +2 -0
  84. package/dist/collectAsync.d.ts +1 -0
  85. package/dist/converters/Cell.d.ts +2 -0
  86. package/dist/converters/ChangeCasingConverter.d.ts +7 -0
  87. package/dist/converters/Constructable.d.ts +1 -0
  88. package/dist/converters/MetaDataRowTypeConverter.d.ts +14 -0
  89. package/dist/converters/PluralizerConverter.d.ts +4 -0
  90. package/dist/converters/RegexConverter.d.ts +7 -0
  91. package/dist/converters/ReplaceConverter.d.ts +7 -0
  92. package/dist/converters/RowTypeConverter.d.ts +10 -0
  93. package/dist/converters/ToLowerCaseConverter.d.ts +4 -0
  94. package/dist/converters/ToUpperCaseConverter.d.ts +4 -0
  95. package/dist/converters/TypeConverter.d.ts +5 -0
  96. package/dist/createCRUDRepositoryOptions.d.ts +7 -0
  97. package/dist/createReadonlyRepositoryMappingOptions.d.ts +6 -0
  98. package/dist/dotEnv.d.ts +5 -0
  99. package/dist/index.d.ts +119 -0
  100. package/dist/mappers/ChangeCasingMapper.d.ts +9 -0
  101. package/dist/mappers/DictionaryMapper.d.ts +8 -0
  102. package/dist/mappers/FlatToNestedMapper.d.ts +20 -0
  103. package/dist/mappers/IFlatToNestedMapper.d.ts +6 -0
  104. package/dist/mappers/IMapper.d.ts +4 -0
  105. package/dist/mappers/IdentityMapper.d.ts +5 -0
  106. package/dist/mappers/PluralizerMapper.d.ts +6 -0
  107. package/dist/mappers/RowTypeMapper.d.ts +6 -0
  108. package/dist/mappers/TransformConverterMapper.d.ts +5 -0
  109. package/dist/mappers/TransformMapper.d.ts +6 -0
  110. package/dist/mappers/TypeMap.d.ts +8 -0
  111. package/dist/mappers/TypeMapGroup.d.ts +5 -0
  112. package/dist/mocks/index.d.ts +9 -0
  113. package/dist/mocks/mock-command.d.ts +28 -0
  114. package/dist/mocks/mock-connection.d.ts +22 -0
  115. package/dist/mocks/mock-datasource.d.ts +24 -0
  116. package/dist/mocks/mock-driver.d.ts +15 -0
  117. package/dist/mocks/mock-reader.d.ts +14 -0
  118. package/dist/mocks/mock-repository.d.ts +42 -0
  119. package/dist/mocks/mock-select-statement.d.ts +10 -0
  120. package/dist/mocks/mock-transaction.d.ts +16 -0
  121. package/dist/mocks/mock-where.d.ts +8 -0
  122. package/dist/parametersToRecord.d.ts +2 -0
  123. package/dist/readerToAsyncIterable.d.ts +2 -0
  124. package/dist/splitStatements.d.ts +1 -0
  125. package/dist/statementParameters.d.ts +1 -0
  126. package/dist/statementUsesParameter.d.ts +1 -0
  127. package/dist/statements/Aggregate.d.ts +6 -0
  128. package/dist/statements/ArrayExpression.d.ts +5 -0
  129. package/dist/statements/BulkDeleteStatement.d.ts +11 -0
  130. package/dist/statements/BulkDeleteStatementTextRenderer.d.ts +4 -0
  131. package/dist/statements/BulkInsertStatement.d.ts +12 -0
  132. package/dist/statements/BulkInsertStatementTextRenderer.d.ts +4 -0
  133. package/dist/statements/BulkUpdateStatement.d.ts +16 -0
  134. package/dist/statements/BulkUpdateStatementTextRenderer.d.ts +8 -0
  135. package/dist/statements/CheckConstraint.d.ts +7 -0
  136. package/dist/statements/Cmp.d.ts +9 -0
  137. package/dist/statements/Column.d.ts +9 -0
  138. package/dist/statements/ColumnDefault.d.ts +5 -0
  139. package/dist/statements/Condition.d.ts +3 -0
  140. package/dist/statements/ConditionRenderer.d.ts +12 -0
  141. package/dist/statements/CreateIndexStatement.d.ts +10 -0
  142. package/dist/statements/CreateIndexStatementTextRenderer.d.ts +5 -0
  143. package/dist/statements/CreateTableStatement.d.ts +11 -0
  144. package/dist/statements/CreateTableStatementTextRenderer.d.ts +18 -0
  145. package/dist/statements/DefaultStatementTextRenderer.d.ts +5 -0
  146. package/dist/statements/DeleteStatement.d.ts +9 -0
  147. package/dist/statements/DeleteStatementTextRenderer.d.ts +7 -0
  148. package/dist/statements/ExecuteStoredProcedureStatement.d.ts +9 -0
  149. package/dist/statements/ExecuteStoredProcedureStatementTextRenderer.d.ts +4 -0
  150. package/dist/statements/ExpressionEvaluator.d.ts +5 -0
  151. package/dist/statements/ExpressionRenderer.d.ts +8 -0
  152. package/dist/statements/ForeignKey.d.ts +19 -0
  153. package/dist/statements/IBulkDeleteStatement.d.ts +3 -0
  154. package/dist/statements/IBulkInsertStatement.d.ts +3 -0
  155. package/dist/statements/IBulkUpdateStatement.d.ts +3 -0
  156. package/dist/statements/ICondition.d.ts +4 -0
  157. package/dist/statements/IConditionVisitor.d.ts +6 -0
  158. package/dist/statements/ICreateIndexStatement.d.ts +3 -0
  159. package/dist/statements/ICreateTableStatement.d.ts +3 -0
  160. package/dist/statements/IDeleteStatement.d.ts +5 -0
  161. package/dist/statements/IExecuteStoredProcedureStatement.d.ts +3 -0
  162. package/dist/statements/IExpression.d.ts +2 -0
  163. package/dist/statements/IInsertStatement.d.ts +3 -0
  164. package/dist/statements/IReturnStatement.d.ts +3 -0
  165. package/dist/statements/ISelectStatement.d.ts +5 -0
  166. package/dist/statements/IStatement.d.ts +4 -0
  167. package/dist/statements/IStatementVisitor.d.ts +26 -0
  168. package/dist/statements/ITableConstraint.d.ts +5 -0
  169. package/dist/statements/IUpdateStatement.d.ts +5 -0
  170. package/dist/statements/IWhere.d.ts +6 -0
  171. package/dist/statements/Identifier.d.ts +5 -0
  172. package/dist/statements/InsertStatement.d.ts +9 -0
  173. package/dist/statements/InsertStatementTextRenderer.d.ts +4 -0
  174. package/dist/statements/Join.d.ts +14 -0
  175. package/dist/statements/Literal.d.ts +6 -0
  176. package/dist/statements/Native.d.ts +5 -0
  177. package/dist/statements/NotNull.d.ts +4 -0
  178. package/dist/statements/Operator.d.ts +107 -0
  179. package/dist/statements/OrderBy.d.ts +10 -0
  180. package/dist/statements/PrimaryKey.d.ts +7 -0
  181. package/dist/statements/ReturnStatement.d.ts +9 -0
  182. package/dist/statements/ReturnStatementTextRenderer.d.ts +4 -0
  183. package/dist/statements/SelectInsertedIdStatement.d.ts +8 -0
  184. package/dist/statements/SelectInsertedIdStatementTextRenderer.d.ts +4 -0
  185. package/dist/statements/SelectStatement.d.ts +20 -0
  186. package/dist/statements/SelectStatementOptions.d.ts +15 -0
  187. package/dist/statements/SelectStatementTextRenderer.d.ts +19 -0
  188. package/dist/statements/StatementEvaluator.d.ts +4 -0
  189. package/dist/statements/StatementParser.d.ts +4 -0
  190. package/dist/statements/StatementRenderer.d.ts +42 -0
  191. package/dist/statements/StatementTextRenderer.d.ts +14 -0
  192. package/dist/statements/Subquery.d.ts +6 -0
  193. package/dist/statements/UniqueConstraint.d.ts +7 -0
  194. package/dist/statements/UpdateStatement.d.ts +14 -0
  195. package/dist/statements/UpdateStatementTextRenderer.d.ts +14 -0
  196. package/dist/statements/Where.d.ts +16 -0
  197. package/dist/statements/WhereText.d.ts +8 -0
  198. package/dist/statements/index.d.ts +75 -0
  199. package/dist/stubs/StubCommand.d.ts +15 -0
  200. package/dist/stubs/StubConnection.d.ts +13 -0
  201. package/dist/stubs/StubDatasource.d.ts +20 -0
  202. package/dist/stubs/StubReader.d.ts +11 -0
  203. package/dist/stubs/StubRepository.d.ts +22 -0
  204. package/dist/stubs/StubTransaction.d.ts +10 -0
  205. package/dist/stubs/createStubDatasource.d.ts +2 -0
  206. package/dist/stubs/index.d.ts +7 -0
  207. package/dist/stubs/notImplemented.d.ts +1 -0
  208. package/package.json +14 -6
  209. package/src/ArrayReader.test.ts +23 -0
  210. package/src/BulkCRUDRepository.test.ts +183 -0
  211. package/src/BulkWriteStrategy.test.ts +293 -0
  212. package/src/CRUDRepository.test.ts +420 -0
  213. package/src/CRUDRepository.ts +1 -1
  214. package/src/CRUDRepositoryOptions.test.ts +76 -0
  215. package/src/Command.test.ts +105 -0
  216. package/src/CommandMiddlewareHost.test.ts +98 -0
  217. package/src/Connection.test.ts +34 -0
  218. package/src/ConsoleCommandMiddleware.test.ts +149 -0
  219. package/src/Datasource.test.ts +63 -0
  220. package/src/DatasourceError.test.ts +10 -0
  221. package/src/DatasourceFactory.test.ts +159 -0
  222. package/src/DatasourceUrlParser.test.ts +173 -0
  223. package/src/ExpectConsoleMiddleware.test.ts +79 -0
  224. package/src/FirstResultReader.test.ts +43 -0
  225. package/src/IsolationLevel.test.ts +13 -0
  226. package/src/ProviderCatalog.test.ts +110 -0
  227. package/src/ProviderNotImplementedError.test.ts +11 -0
  228. package/src/ProviderNotInstalledError.test.ts +11 -0
  229. package/src/ReadonlyRepository.test.ts +406 -0
  230. package/src/RepositoryFactory.test.ts +94 -0
  231. package/src/StatementMappers.test.ts +221 -0
  232. package/src/StreamingReader.test.ts +126 -0
  233. package/src/Transaction.test.ts +37 -0
  234. package/src/UnitOfWorkTransaction.test.ts +43 -0
  235. package/src/UserViewCRUDRepository.test.ts +323 -0
  236. package/src/builders/builders.test.ts +404 -0
  237. package/src/converters/ChangeCasingConverter.test.ts +34 -0
  238. package/src/converters/PluralizerConverter.test.ts +43 -0
  239. package/src/converters/RegexConverter.test.ts +11 -0
  240. package/src/converters/ReplaceConverter.test.ts +9 -0
  241. package/src/converters/RowTypeConverter.test.ts +79 -0
  242. package/src/converters/ToLowerCaseConverter.test.ts +9 -0
  243. package/src/converters/ToUpperCaseConverter.test.ts +9 -0
  244. package/src/dotEnv.test.ts +21 -0
  245. package/src/edgeCases.test.ts +485 -0
  246. package/src/mappers/ChangeCasingMapper.test.ts +32 -0
  247. package/src/mappers/DictionaryMapper.test.ts +69 -0
  248. package/src/mappers/FlatToNestedMapper.test.ts +269 -0
  249. package/src/mappers/IdentityMapper.test.ts +12 -0
  250. package/src/mappers/PluralizerMapper.test.ts +40 -0
  251. package/src/mappers/RowTypeMapper.test.ts +12 -0
  252. package/src/mappers/TransformConverterMapper.test.ts +181 -0
  253. package/src/mappers/transformMapper.test.ts +66 -0
  254. package/src/mocks/MockCommand.test.ts +75 -0
  255. package/src/mocks/MockConnection.test.ts +34 -0
  256. package/src/mocks/MockDatasource.test.ts +40 -0
  257. package/src/mocks/MockReader.test.ts +17 -0
  258. package/src/mocks/MockRepository.test.ts +32 -0
  259. package/src/mocks/MockSelectStatement.test.ts +11 -0
  260. package/src/mocks/MockTransaction.test.ts +17 -0
  261. package/src/mocks/MockWhere.test.ts +11 -0
  262. package/src/parametersToRecord.test.ts +19 -0
  263. package/src/public-surface.test.ts +23 -0
  264. package/src/readWithWhereClauseAndCustomFlatToNestedObjectMappingOptions.test.ts +126 -0
  265. package/src/readerToAsyncIterable.test.ts +39 -0
  266. package/src/repository.test.ts +410 -0
  267. package/src/repository.type.test.ts +29 -0
  268. package/src/statements/SelectInsertedIdStatement.test.ts +11 -0
  269. package/src/statements/StatementCompile.test.ts +231 -0
  270. package/src/statements/StatementParser.test.ts +304 -0
  271. package/src/statements/index.ts +1 -1
  272. package/src/stubs/StubDatasource.test.ts +99 -0
  273. package/src/tests/contact/Address.ts +14 -0
  274. package/src/tests/contact/AddressType.ts +6 -0
  275. package/src/tests/contact/Base.ts +7 -0
  276. package/src/tests/contact/Contact.ts +12 -0
  277. package/src/tests/contact/ContactBase.ts +8 -0
  278. package/src/tests/contact/ContactChangeLog.ts +8 -0
  279. package/src/tests/contact/ContactGroup.ts +6 -0
  280. package/src/tests/contact/ContactGroupBase.ts +6 -0
  281. package/src/tests/contact/ContactGroupMember.ts +6 -0
  282. package/src/tests/contact/ContactSource.ts +6 -0
  283. package/src/tests/contact/ContactType.ts +6 -0
  284. package/src/tests/contact/ContactsService.test.ts +118 -0
  285. package/src/tests/contact/ContactsService.ts +32 -0
  286. package/src/tests/contact/ImportContactRequest.ts +4 -0
  287. package/src/tests/contact/ImportContactResult.ts +5 -0
  288. package/src/tests/contact/LegacyContact.ts +7 -0
  289. package/src/tests/contact/LegacyMigrationResult.ts +5 -0
  290. package/src/tests/contact/Phone.ts +9 -0
  291. package/src/tests/contact/PhoneType.ts +6 -0
  292. package/src/tests/contact/asContactsView.ts +63 -0
  293. package/src/tests/contact/contactColumns.ts +40 -0
  294. package/src/tests/contact/contactLookupIds.ts +32 -0
  295. package/src/tests/contact/contactLookups.ts +6 -0
  296. package/src/tests/contact/contactsParity.ts +63 -0
  297. package/src/tests/contact/contactsServiceOf.ts +38 -0
  298. package/src/tests/contact/contactsView.ts +31 -0
  299. package/src/tests/contact/expectContactsView.ts +23 -0
  300. package/src/tests/contact/extraContact.ts +61 -0
  301. package/src/tests/contact/logMapped.ts +5 -0
  302. package/src/tests/contact/seedContacts.ts +31 -0
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { CommandMiddlewareHost } from "./index.ts";
3
+
4
+ describe("CommandMiddlewareHost", () => {
5
+ it("base execute methods require a driver", () => {
6
+ const host = new CommandMiddlewareHost();
7
+ expect(() => host.execute("SELECT id FROM users")).toThrow("executeNative");
8
+ expect(() => host.execute(" ")).toThrow("empty command");
9
+ expect(() => host.executeReader("SELECT id FROM users")).toThrow("executeReaderNative");
10
+ expect(() => host.executeReader("")).toThrow("empty command");
11
+ });
12
+
13
+ it("rewrites the text and runs afterCommand", async () => {
14
+ const host = new CommandMiddlewareHost();
15
+ const seen: string[] = [];
16
+ let used = "";
17
+ host.addMiddleware({
18
+ beforeCommand: (text) => {
19
+ seen.push(`before:${text}`);
20
+ return { text: "SELECT 2" };
21
+ },
22
+ afterCommand: (text, _params, results, elapsedMs) => {
23
+ seen.push(`after:${text}:${results.length}:${elapsedMs >= 0}`);
24
+ },
25
+ });
26
+ const result = await host.run("SELECT 1", undefined, async (text) => {
27
+ used = text;
28
+ seen.push("op");
29
+ return { value: 7, rows: [{ id: 1 }] };
30
+ });
31
+ expect(result).toBe(7);
32
+ expect(used).toBe("SELECT 2");
33
+ expect(seen[0]).toBe("before:SELECT 1");
34
+ expect(seen[1]).toBe("op");
35
+ expect(seen[2]?.startsWith("after:SELECT 2:1:true")).toBe(true);
36
+ });
37
+
38
+ it("runs with no middleware", async () => {
39
+ const host = new CommandMiddlewareHost();
40
+ expect(await host.run("", undefined, async () => ({ value: "ok", rows: [] }))).toBe(
41
+ "ok",
42
+ );
43
+ });
44
+
45
+ it("skips the command when beforeCommand throws", async () => {
46
+ const host = new CommandMiddlewareHost();
47
+ let ran = false;
48
+ host.addMiddleware({
49
+ beforeCommand: () => {
50
+ throw new Error("blocked");
51
+ },
52
+ afterCommand: () => {
53
+ ran = true;
54
+ },
55
+ });
56
+ await expect(
57
+ host.run("select", undefined, async () => {
58
+ ran = true;
59
+ return { value: 1, rows: [] };
60
+ }),
61
+ ).rejects.toThrow("blocked");
62
+ expect(ran).toBe(false);
63
+ });
64
+
65
+ it("runs afterCommand with error when the command fails", async () => {
66
+ const host = new CommandMiddlewareHost();
67
+ let captured: Error | undefined;
68
+ host.addMiddleware({
69
+ beforeCommand: () => undefined,
70
+ afterCommand: (_q, _p, _r, _e, error) => {
71
+ captured = error;
72
+ },
73
+ });
74
+ await expect(
75
+ host.run("select", undefined, async () => {
76
+ throw new Error("fail-op");
77
+ }),
78
+ ).rejects.toThrow("fail-op");
79
+ expect(captured?.message).toBe("fail-op");
80
+ });
81
+
82
+ it("wraps a non-Error throw for afterCommand", async () => {
83
+ const host = new CommandMiddlewareHost();
84
+ let captured: Error | undefined;
85
+ host.addMiddleware({
86
+ beforeCommand: () => undefined,
87
+ afterCommand: (_q, _p, _r, _e, error) => {
88
+ captured = error;
89
+ },
90
+ });
91
+ await expect(
92
+ host.run("select", undefined, async () => {
93
+ throw "string-fail";
94
+ }),
95
+ ).rejects.toBe("string-fail");
96
+ expect(captured?.message).toBe("string-fail");
97
+ });
98
+ });
@@ -0,0 +1,34 @@
1
+ import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { Datasource, DatasourceFactory, MockDriver } from "./index.ts";
3
+
4
+ const envKeys = [
5
+ "INSTALLED_PROVIDERS",
6
+ "DATASOURCE_URL",
7
+ "PROVIDER",
8
+ ] as const;
9
+ const savedEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
10
+
11
+ beforeEach(() => {
12
+ for (const key of envKeys) delete process.env[key];
13
+ DatasourceFactory.register("sqlite", new MockDriver());
14
+ process.env.PROVIDER = "sqlite";
15
+ });
16
+
17
+ afterEach(() => DatasourceFactory.reset());
18
+
19
+ afterAll(() => {
20
+ for (const key of envKeys) {
21
+ if (savedEnv[key] === undefined) delete process.env[key];
22
+ else process.env[key] = savedEnv[key];
23
+ }
24
+ });
25
+
26
+ describe("Connection", () => {
27
+ it("create from default", async () => {
28
+ const ds = Datasource.default;
29
+ const connection = await ds.createConnection();
30
+ await connection.open();
31
+ await connection.close();
32
+ expect(connection.isOpen).toBe(false);
33
+ });
34
+ });
@@ -0,0 +1,149 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { CommandMiddlewareHost, ConsoleCommandMiddleware, formatElapsed } from "./index.ts";
3
+
4
+ describe("ConsoleCommandMiddleware", () => {
5
+ it("formats elapsed time as ms or μs", () => {
6
+ expect(formatElapsed(2)).toBe("2ms");
7
+ expect(formatElapsed(1)).toBe("1ms");
8
+ expect(formatElapsed(0.85)).toBe("850μs");
9
+ expect(formatElapsed(0.4)).toBe("400μs");
10
+ });
11
+
12
+ it("logs Start and Finish with an injected clock", async () => {
13
+ const lines: string[] = [];
14
+ const stamps = [
15
+ new Date("2026-08-28T01:54:12.101Z"),
16
+ new Date("2026-08-28T01:54:12.103Z"),
17
+ ];
18
+ const middleware = new ConsoleCommandMiddleware(
19
+ (line) => lines.push(line),
20
+ () => stamps.shift() ?? new Date("2026-08-28T01:54:12.103Z"),
21
+ );
22
+ const host = new CommandMiddlewareHost();
23
+ host.addMiddleware(middleware);
24
+ await host.run('SELECT * FROM "widget" ORDER BY "id"', undefined, async () => {
25
+ await new Promise((resolve) => setTimeout(resolve, 2));
26
+ return { value: 1, rows: [] };
27
+ });
28
+ expect(lines[0]).toBe(
29
+ '[2026-08-28T01:54:12.101Z]-[datasource][Start]-[SELECT * FROM "widget" ORDER BY "id"]',
30
+ );
31
+ expect(lines[1]).toMatch(
32
+ /^\[2026-08-28T01:54:12.103Z\]-\[datasource\]\[Finish\]-\[SELECT \* FROM "widget" ORDER BY "id"\] \d+(ms|μs)$/,
33
+ );
34
+ });
35
+
36
+ it("uses console.log and Date by default", async () => {
37
+ const logged: string[] = [];
38
+ const spy = vi.spyOn(console, "log").mockImplementation((line) => {
39
+ logged.push(String(line));
40
+ });
41
+ const middleware = new ConsoleCommandMiddleware();
42
+ middleware.beforeCommand("SELECT 1");
43
+ middleware.afterCommand("SELECT 1", undefined, [], 2);
44
+ expect(logged[0]).toMatch(
45
+ /^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\]-\[datasource\]\[Start\]-\[SELECT 1\]$/,
46
+ );
47
+ expect(logged[1]).toMatch(/\[Finish\]-\[SELECT 1\] 2ms$/);
48
+ spy.mockRestore();
49
+ });
50
+
51
+ it("trims trailing newlines before logging", async () => {
52
+ const lines: string[] = [];
53
+ const host = new CommandMiddlewareHost();
54
+ host.addMiddleware(new ConsoleCommandMiddleware((line) => lines.push(line)));
55
+ await host.run("SELECT 1\n", undefined, async () => ({ value: 1, rows: [] }));
56
+ expect(lines[0].replace(/^\[[^\]]+\]-/, "")).toBe("[datasource][Start]-[SELECT 1]");
57
+ });
58
+
59
+ it("logs Request and AfterCommand when isVerbose", async () => {
60
+ const lines: string[] = [];
61
+ const stamps = [
62
+ new Date("2026-08-28T01:54:12.101Z"),
63
+ new Date("2026-08-28T01:54:12.102Z"),
64
+ new Date("2026-08-28T01:54:12.103Z"),
65
+ new Date("2026-08-28T01:54:12.104Z"),
66
+ ];
67
+ const host = new CommandMiddlewareHost();
68
+ host.addMiddleware(
69
+ new ConsoleCommandMiddleware(
70
+ (line) => lines.push(line),
71
+ () => stamps.shift() ?? new Date("2026-08-28T01:54:12.104Z"),
72
+ { isVerbose: "true" },
73
+ ),
74
+ );
75
+ await host.run("SELECT 1", [{ name: "id", value: 1 }], async () => ({
76
+ value: 1,
77
+ rows: [{ id: 1 }],
78
+ }));
79
+ expect(lines).toHaveLength(4);
80
+ expect(lines[0]).toBe("[2026-08-28T01:54:12.101Z]-[datasource][Start]-[SELECT 1]");
81
+ expect(lines[1]).toBe("[2026-08-28T01:54:12.102Z]-[datasource][Request]-[{id: 1}]");
82
+ expect(lines[2]).toMatch(
83
+ /^\[2026-08-28T01:54:12.103Z\]-\[datasource\]\[Finish\]-\[SELECT 1\] \d+(ms|μs)$/,
84
+ );
85
+ expect(lines[3]).toBe("[2026-08-28T01:54:12.104Z]-[datasource][AfterCommand]-[{id: 1}]");
86
+ lines.length = 0;
87
+ await host.run("DELETE FROM user", [{ name: "id", value: 2 }], async () => ({
88
+ value: { result: null, rowsAffected: 1 },
89
+ rows: [],
90
+ }));
91
+ expect(lines[3]).toBe(
92
+ "[2026-08-28T01:54:12.104Z]-[datasource][AfterCommand]-[{result: null, rowsAffected: 1}]",
93
+ );
94
+ });
95
+
96
+ it("serializes Date, binary, and bigint in verbose bodies", () => {
97
+ const lines: string[] = [];
98
+ const middleware = new ConsoleCommandMiddleware(
99
+ (line) => lines.push(line),
100
+ () => new Date("2026-08-28T01:54:12.101Z"),
101
+ { isVerbose: "1" },
102
+ );
103
+ middleware.beforeCommand("SELECT 1");
104
+ expect(lines[1]).toBe("[2026-08-28T01:54:12.101Z]-[datasource][Request]-[{}]");
105
+ lines.length = 0;
106
+ middleware.beforeCommand("SELECT 1", [
107
+ { name: "when", value: new Date("2026-08-28T01:54:12.000Z") },
108
+ { name: "blob", value: new Uint8Array([1, 2, 3]) },
109
+ { name: "n", value: 1n },
110
+ ]);
111
+ middleware.afterCommand(
112
+ "SELECT 1",
113
+ undefined,
114
+ [
115
+ {
116
+ when: new Date("2026-08-28T01:54:12.000Z"),
117
+ blob: new Uint8Array([1, 2, 3]),
118
+ n: 1n,
119
+ },
120
+ ],
121
+ 2,
122
+ );
123
+ expect(lines[1]).toBe(
124
+ "[2026-08-28T01:54:12.101Z]-[datasource][Request]-[{when: 2026-08-28T01:54:12.000Z, blob: AQID, n: 1}]",
125
+ );
126
+ expect(lines[3]).toBe(
127
+ "[2026-08-28T01:54:12.101Z]-[datasource][AfterCommand]-[{blob: AQID, n: 1, when: 2026-08-28T01:54:12.000Z}]",
128
+ );
129
+ lines.length = 0;
130
+ middleware.beforeCommand("SELECT 1", [
131
+ { name: "name", value: "Ada" },
132
+ { name: "missing", value: null },
133
+ { name: "on", value: true },
134
+ { name: "off", value: false },
135
+ ]);
136
+ middleware.afterCommand(
137
+ "SELECT 1",
138
+ undefined,
139
+ [{ name: "Ada", missing: null, on: true, off: false }],
140
+ 2,
141
+ );
142
+ expect(lines[1]).toBe(
143
+ "[2026-08-28T01:54:12.101Z]-[datasource][Request]-[{name: Ada, missing: null, on: True, off: False}]",
144
+ );
145
+ expect(lines[3]).toBe(
146
+ "[2026-08-28T01:54:12.101Z]-[datasource][AfterCommand]-[{missing: null, name: Ada, off: False, on: True}]",
147
+ );
148
+ });
149
+ });
@@ -0,0 +1,63 @@
1
+ import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { Datasource, DatasourceFactory, MockConnection, MockDriver } from "./index.ts";
3
+
4
+ const envKeys = [
5
+ "INSTALLED_PROVIDERS",
6
+ "DATASOURCE_URL",
7
+ "PROVIDER",
8
+ ] as const;
9
+ const savedEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
10
+
11
+ beforeEach(() => {
12
+ for (const key of envKeys) delete process.env[key];
13
+ DatasourceFactory.register("sqlite", new MockDriver());
14
+ process.env.PROVIDER = "sqlite";
15
+ });
16
+
17
+ afterEach(() => DatasourceFactory.reset());
18
+
19
+ afterAll(() => {
20
+ for (const key of envKeys) {
21
+ if (savedEnv[key] === undefined) delete process.env[key];
22
+ else process.env[key] = savedEnv[key];
23
+ }
24
+ });
25
+
26
+ describe("Datasource", () => {
27
+ it("create default from env settings", async () => {
28
+ const ds = Datasource.default;
29
+ const command = await ds.createCommand();
30
+ expect(ds.provider).toBe("sqlite");
31
+ expect(command.connection).toBe(await ds.createConnection());
32
+ expect(command.connection.isOpen).toBe(true);
33
+ class Wide {
34
+ id = 0;
35
+ on = false;
36
+ when = new Date(0);
37
+ blob = new Uint8Array();
38
+ tags: string[] = [];
39
+ name = "";
40
+ }
41
+ expect(ds.createRepository(Wide)).toBeTruthy();
42
+ });
43
+
44
+ it("leaves a passed connection closed", async () => {
45
+ const ds = Datasource.default;
46
+ const given = new MockConnection();
47
+ expect((await ds.createCommand(given)).connection).toBe(given);
48
+ expect(given.isOpen).toBe(false);
49
+ await ds.createCommand();
50
+ await expect(ds.createCommand()).rejects.toThrow("Connection is already open");
51
+ });
52
+
53
+ it("applies repository options for crud and readonly", () => {
54
+ const ds = Datasource.default;
55
+ class Item {
56
+ id = 0;
57
+ name = "";
58
+ }
59
+ expect(ds.createRepository(Item, undefined, { entity: "items", ids: ["id"] })).toBeTruthy();
60
+ expect(ds.createReadonlyRepository(Item, undefined, { entity: "items" })).toBeTruthy();
61
+ expect(ds.createReadonlyRepository(Item)).toBeTruthy();
62
+ });
63
+ });
@@ -0,0 +1,10 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { DatasourceError } from "./index.ts";
3
+
4
+ describe("DatasourceError", () => {
5
+ it("wraps a generic datasource error", () => {
6
+ const error = new DatasourceError("broken");
7
+ expect(error.name).toBe("DatasourceError");
8
+ expect(error.message).toBe("broken");
9
+ });
10
+ });
@@ -0,0 +1,159 @@
1
+ import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import {
3
+ CRUDRepository,
4
+ Datasource,
5
+ DatasourceError,
6
+ DatasourceFactory,
7
+ MockDriver,
8
+ ProviderNotInstalledError,
9
+ ReadonlyRepository,
10
+ untyped,
11
+ } from "./index.ts";
12
+ import type { IConnection } from "./IConnection.ts";
13
+ import { MockCommand } from "./mocks/mock-command.ts";
14
+ import { MockConnection } from "./mocks/mock-connection.ts";
15
+
16
+ const selectText = async (
17
+ repo: { read(): AsyncIterable<unknown> },
18
+ connection: IConnection,
19
+ ): Promise<string | undefined> => {
20
+ if (!(connection instanceof MockConnection)) throw new Error("expected mock connection");
21
+ const command = new MockCommand(connection);
22
+ connection.command = command;
23
+ for await (const item of repo.read()) void item;
24
+ return command.executeReaderCalls[0]?.text;
25
+ };
26
+
27
+ const envKeys = [
28
+ "INSTALLED_PROVIDERS",
29
+ "DATASOURCE_URL",
30
+ "PROVIDER",
31
+ ] as const;
32
+ const savedEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
33
+
34
+ beforeEach(() => {
35
+ for (const key of envKeys) delete process.env[key];
36
+ });
37
+
38
+ afterEach(() => {
39
+ DatasourceFactory.reset();
40
+ });
41
+
42
+ afterAll(() => {
43
+ for (const key of envKeys) {
44
+ if (savedEnv[key] === undefined) delete process.env[key];
45
+ else process.env[key] = savedEnv[key];
46
+ }
47
+ });
48
+
49
+ describe("DatasourceFactory", () => {
50
+ it("default from environment", () => {
51
+ DatasourceFactory.register("sqlite", new MockDriver());
52
+ process.env.PROVIDER = "sqlite";
53
+ const ds = Datasource.default;
54
+ expect(ds.provider).toBe("sqlite");
55
+ });
56
+
57
+ it("skips a repeated installed provider", () => {
58
+ DatasourceFactory.defaults({ INSTALLED_PROVIDERS: "memory,memory" });
59
+ expect(DatasourceFactory.registered()).toEqual(["memory"]);
60
+ });
61
+
62
+ it("missing provider throws", () => {
63
+ expect(() => DatasourceFactory.create({ PROVIDER: "missing" })).toThrow(
64
+ ProviderNotInstalledError,
65
+ );
66
+ });
67
+
68
+ it("mock driver builds command and transaction", () => {
69
+ const driver = new MockDriver();
70
+ expect(driver.rowTypeConverters).toEqual({});
71
+ const connection = driver.connection();
72
+ expect(driver.command(connection)).toBeInstanceOf(MockCommand);
73
+ expect(driver.transaction(connection).isolation).toBe("readCommitted");
74
+ expect(driver.transaction(connection, "readCommitted").isolation).toBe(
75
+ "readCommitted",
76
+ );
77
+ });
78
+ });
79
+
80
+ describe("Datasource registry", () => {
81
+ it("registers, lists, creates, and resets", () => {
82
+ expect(DatasourceFactory.registered()).toEqual([]);
83
+ DatasourceFactory.register("json", new MockDriver());
84
+ DatasourceFactory.register("memory", new MockDriver());
85
+ expect(DatasourceFactory.registered()).toEqual(["json", "memory"]);
86
+ const created = Datasource.create({ PROVIDER: "json", DATASOURCE_URL: "json://x" });
87
+ expect(created.provider).toBe("json");
88
+ DatasourceFactory.reset();
89
+ expect(DatasourceFactory.registered()).toEqual([]);
90
+ expect(() => Datasource.create({ PROVIDER: "json" })).toThrow(ProviderNotInstalledError);
91
+ });
92
+
93
+ it("requires consumer defaults when provider is omitted", async () => {
94
+ expect(() => Datasource.create()).toThrow(DatasourceError);
95
+ DatasourceFactory.register("sqlite", new MockDriver());
96
+ const ds = Datasource.create({
97
+ PROVIDER: "sqlite",
98
+ DATASOURCE_URL: "sqlite:///:memory:",
99
+ });
100
+ expect(ds.provider).toBe("sqlite");
101
+ const connection = await ds.createConnection();
102
+ expect(connection.isOpen).toBe(false);
103
+ expect((await ds.createCommand()).connection).toBe(connection);
104
+ expect(ds.createRepository(untyped())).toBeInstanceOf(CRUDRepository);
105
+ expect(ds.createReadonlyRepository(untyped()).constructor).toBe(ReadonlyRepository);
106
+ class Item {
107
+ id = 0;
108
+ name = "";
109
+ }
110
+ const typed = ds.createRepository(Item, connection);
111
+ const typedRead = ds.createReadonlyRepository(Item);
112
+ expect(typed).toBeInstanceOf(CRUDRepository);
113
+ expect(typed.type).toBe(Item);
114
+ expect(await selectText(typed, connection)).toBe("SELECT id, name FROM Item");
115
+ expect(typedRead.constructor).toBe(ReadonlyRepository);
116
+ expect(typedRead.type).toBe(Item);
117
+ expect(await selectText(typedRead, connection)).toBe("SELECT id, name FROM Item");
118
+ const enriched = ds.createReadonlyRepository(Item) as ReadonlyRepository<Item>;
119
+ expect(enriched).toBeInstanceOf(ReadonlyRepository);
120
+ typed.addLinked({ destinationProperty: "name", sourceProperty: "id", relatedKey: "id", association: { parent: "id", related: "name" } }, typedRead, typed);
121
+ const withWrite = ds.createRepository(Item, connection, {
122
+ write: { useTransaction: false, persist: async () => {} },
123
+ ids: ["id"],
124
+ });
125
+ expect(withWrite).toBeInstanceOf(CRUDRepository);
126
+ const withCrudIds = ds.createRepository(Item, connection, {
127
+ ids: undefined as never,
128
+ entity: "Item",
129
+ });
130
+ expect(withCrudIds).toBeInstanceOf(CRUDRepository);
131
+ const withReadOpts = ds.createRepository(Item, connection, {
132
+ entity: "items",
133
+ });
134
+ expect(withReadOpts).toBeInstanceOf(CRUDRepository);
135
+ expect(new DatasourceFactory().create({
136
+ PROVIDER: "sqlite",
137
+ }).provider).toBe("sqlite");
138
+ process.env.DATASOURCE_URL = " ";
139
+ expect(() => Datasource.create()).toThrow(DatasourceError);
140
+ process.env.DATASOURCE_URL = "sqlite:///:memory:";
141
+ expect(() => Datasource.create()).toThrow(ProviderNotInstalledError);
142
+ DatasourceFactory.defaults({ INSTALLED_PROVIDERS: "json" });
143
+ expect(DatasourceFactory.registered()).toEqual(["json", "sqlite"]);
144
+ DatasourceFactory.reset();
145
+ for (const key of envKeys) delete process.env[key];
146
+ expect(() => Datasource.create()).toThrow("Datasource defaults are not specified");
147
+ expect(() => Datasource.default).toThrow("Datasource defaults are not specified");
148
+ DatasourceFactory.register("sqlite", new MockDriver());
149
+ DatasourceFactory.register("memory", new MockDriver());
150
+ process.env.DATASOURCE_URL = "sqlite:///:memory:";
151
+ expect(() => Datasource.default).toThrow(ProviderNotInstalledError);
152
+ delete process.env.DATASOURCE_URL;
153
+ process.env.PROVIDER = "sqlite";
154
+ expect(Datasource.default.provider).toBe("sqlite");
155
+ process.env.PROVIDER = "memory";
156
+ process.env.DATASOURCE_URL = "memory://";
157
+ expect(Datasource.default.provider).toBe("memory");
158
+ });
159
+ });
@@ -0,0 +1,173 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { DatasourceUrlParser } from "./index.ts";
3
+
4
+ describe("DatasourceUrlParser", () => {
5
+ it("postgres url", () => {
6
+ const url = DatasourceUrlParser.parse(
7
+ "postgres://ds:ds@127.0.0.1:5432/datasources?schema=public&sslmode=require",
8
+ );
9
+ expect(url?.provider).toBe("postgres");
10
+ expect(url?.username).toBe("ds");
11
+ expect(url?.password).toBe("ds");
12
+ expect(url?.host).toBe("127.0.0.1");
13
+ expect(url?.port).toBe(5432);
14
+ expect(url?.path).toBe("/datasources");
15
+ expect(url?.datasource).toBe("datasources");
16
+ expect(url?.schema).toBe("public");
17
+ expect(url?.options.sslmode).toBe("require");
18
+ });
19
+
20
+ it("postgres user without password", () => {
21
+ const url = DatasourceUrlParser.parse("postgres://ds@127.0.0.1/datasources");
22
+ expect(url?.username).toBe("ds");
23
+ expect(url?.password).toBeUndefined();
24
+ expect(url?.port).toBeUndefined();
25
+ expect(url?.schema).toBeUndefined();
26
+ expect(url?.options).toEqual({});
27
+ });
28
+
29
+ it("schema only", () => {
30
+ const url = DatasourceUrlParser.parse("postgres://127.0.0.1/datasources?schema=public");
31
+ expect(url?.schema).toBe("public");
32
+ expect(url?.options).toEqual({});
33
+ });
34
+
35
+ it("options only", () => {
36
+ const url = DatasourceUrlParser.parse("postgres://127.0.0.1/datasources?sslmode=require");
37
+ expect(url?.schema).toBeUndefined();
38
+ expect(url?.options.sslmode).toBe("require");
39
+ });
40
+
41
+ it("repeated option keeps the last value", () => {
42
+ const url = DatasourceUrlParser.parse(
43
+ "postgres://127.0.0.1/datasources?sslmode=require&sslmode=disable",
44
+ );
45
+ expect(url?.options.sslmode).toBe("disable");
46
+ });
47
+
48
+ it("flag option is empty", () => {
49
+ const url = DatasourceUrlParser.parse("postgres://127.0.0.1/datasources?readonly");
50
+ expect(url?.options.readonly).toBe("");
51
+ });
52
+
53
+ it("mysql url", () => {
54
+ const url = DatasourceUrlParser.parse("mysql://ds:secret@127.0.0.1:3306/app");
55
+ expect(url?.provider).toBe("mysql");
56
+ expect(url?.username).toBe("ds");
57
+ expect(url?.password).toBe("secret");
58
+ expect(url?.host).toBe("127.0.0.1");
59
+ expect(url?.port).toBe(3306);
60
+ expect(url?.datasource).toBe("app");
61
+ });
62
+
63
+ it("mysql host and datasource", () => {
64
+ const url = DatasourceUrlParser.parse("mysql://db/app");
65
+ expect(url?.host).toBe("db");
66
+ expect(url?.datasource).toBe("app");
67
+ expect(url?.username).toBeUndefined();
68
+ expect(url?.port).toBeUndefined();
69
+ });
70
+
71
+ it("sqlite relative file", () => {
72
+ const url = DatasourceUrlParser.parse("sqlite:///app.db");
73
+ expect(url?.provider).toBe("sqlite");
74
+ expect(url?.path).toBe("/app.db");
75
+ expect(url?.datasource).toBe("app.db");
76
+ expect(url?.host).toBeUndefined();
77
+ });
78
+
79
+ it("sqlite absolute file", () => {
80
+ const url = DatasourceUrlParser.parse("sqlite:////tmp/app.db");
81
+ expect(url?.path).toBe("//tmp/app.db");
82
+ expect(url?.datasource).toBe("/tmp/app.db");
83
+ });
84
+
85
+ it("sqlite memory", () => {
86
+ const url = DatasourceUrlParser.parse("sqlite:///:memory:");
87
+ expect(url?.provider).toBe("sqlite");
88
+ expect(url?.path).toBe("/:memory:");
89
+ expect(url?.datasource).toBe(":memory:");
90
+ });
91
+
92
+ it("sqlite memory without path", () => {
93
+ const url = DatasourceUrlParser.parse("sqlite://:memory:");
94
+ expect(url?.host).toBe(":memory:");
95
+ expect(url?.datasource).toBeUndefined();
96
+ });
97
+
98
+ it("json file", () => {
99
+ const url = DatasourceUrlParser.parse("json://contacts.json");
100
+ expect(url?.provider).toBe("json");
101
+ expect(url?.datasource).toBe("contacts.json");
102
+ expect(url?.host).toBeUndefined();
103
+ });
104
+
105
+ it("percent encoded password", () => {
106
+ const url = DatasourceUrlParser.parse("postgres://ds:p%3Ax@127.0.0.1/datasources");
107
+ expect(url?.password).toBe("p:x");
108
+ });
109
+
110
+ it("password contains colon", () => {
111
+ const url = DatasourceUrlParser.parse("postgres://ds:a:b@127.0.0.1/datasources");
112
+ expect(url?.username).toBe("ds");
113
+ expect(url?.password).toBe("a:b");
114
+ });
115
+
116
+ it("edges", () => {
117
+ const upper = DatasourceUrlParser.parse("Postgres://127.0.0.1/db");
118
+ expect(upper?.provider).toBe("postgres");
119
+ const opaque = DatasourceUrlParser.parse("a.b+c1:file");
120
+ expect(opaque?.provider).toBe("a.b+c1");
121
+ expect(opaque?.datasource).toBe("file");
122
+ const slash = DatasourceUrlParser.parse("postgres://127.0.0.1/");
123
+ expect(slash?.path).toBe("/");
124
+ expect(slash?.datasource).toBeUndefined();
125
+ const ipv6 = DatasourceUrlParser.parse("postgres://[::1]:5432/db");
126
+ expect(ipv6?.host).toBe("::1");
127
+ expect(ipv6?.port).toBe(5432);
128
+ const hostOnly = DatasourceUrlParser.parse("postgres://[::1]/db");
129
+ expect(hostOnly?.host).toBe("::1");
130
+ expect(hostOnly?.port).toBeUndefined();
131
+ const noHost = DatasourceUrlParser.parse("postgres://ds@/db");
132
+ expect(noHost?.host).toBeUndefined();
133
+ const portOnly = DatasourceUrlParser.parse("postgres://:5432/db");
134
+ expect(portOnly?.host).toBeUndefined();
135
+ expect(portOnly?.port).toBe(5432);
136
+ const passwordOnly = DatasourceUrlParser.parse("postgres://:secret@h/db");
137
+ expect(passwordOnly?.username).toBeUndefined();
138
+ expect(passwordOnly?.password).toBe("secret");
139
+ const fragment = DatasourceUrlParser.parse("postgres://h/db#frag");
140
+ expect(fragment?.datasource).toBe("db");
141
+ const accented = DatasourceUrlParser.parse("postgres://dé@h/db");
142
+ expect(accented?.username).toBe("dé");
143
+ const encoded = DatasourceUrlParser.parse("postgres://d%C3%A9@h/db");
144
+ expect(encoded?.username).toBe("dé");
145
+ expect(DatasourceUrlParser.parse(undefined)).toBeUndefined();
146
+ expect(DatasourceUrlParser.parse(" ")).toBeUndefined();
147
+ expect(DatasourceUrlParser.parse("9abc:file")).toBeUndefined();
148
+ expect(DatasourceUrlParser.parse("postgres://ds:a%zz@h/db")).toBeUndefined();
149
+ expect(DatasourceUrlParser.parse("postgres://[/db")).toBeUndefined();
150
+ expect(DatasourceUrlParser.parse("postgres://[::1]x/db")).toBeUndefined();
151
+ expect(DatasourceUrlParser.parse("postgres://[::1]:no/db")).toBeUndefined();
152
+ expect(DatasourceUrlParser.parse("postgres://[%zz]/db")).toBeUndefined();
153
+ const emptyUser = DatasourceUrlParser.parse("postgres://@h/db");
154
+ expect(emptyUser?.username).toBeUndefined();
155
+ expect(emptyUser?.host).toBe("h");
156
+ expect(DatasourceUrlParser.parse("postgres://ds:a%@h/db")).toBeUndefined();
157
+ expect(DatasourceUrlParser.parse("postgres://%zz@h/db")).toBeUndefined();
158
+ expect(DatasourceUrlParser.parse("postgres://%zz:5432/db")).toBeUndefined();
159
+ expect(DatasourceUrlParser.parse("json://%zz")).toBeUndefined();
160
+ expect(DatasourceUrlParser.parse("postgres://%zz/db")).toBeUndefined();
161
+ expect(DatasourceUrlParser.parse("postgres://h/db?sslmode=%zz")).toBeUndefined();
162
+ expect(DatasourceUrlParser.parse("postgres://h/db?&sslmode=on")?.options.sslmode).toBe("on");
163
+ });
164
+
165
+ it("not a url", () => {
166
+ const blank = DatasourceUrlParser.parse("");
167
+ const text = DatasourceUrlParser.parse("not-a-url");
168
+ const connectionString = DatasourceUrlParser.parse("Host=db;Port=1");
169
+ expect(blank).toBeUndefined();
170
+ expect(text).toBeUndefined();
171
+ expect(connectionString).toBeUndefined();
172
+ });
173
+ });