@shortlink-org/portolan 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 (381) hide show
  1. package/README.md +69 -205
  2. package/cli/init.mjs +285 -0
  3. package/cli/init.test.mjs +158 -0
  4. package/cli/portolan.mjs +37 -73
  5. package/index.html +19 -0
  6. package/package.json +5 -10
  7. package/plugins/README.md +66 -27
  8. package/plugins/portolan-go.wasm +0 -0
  9. package/portolan.json +52 -73
  10. package/public/landing/cats/bug-hunter.webp +0 -0
  11. package/public/landing/cats/compass-nap.webp +0 -0
  12. package/public/landing/cats/diagrammer.webp +0 -0
  13. package/public/landing/cats/docs-reader.webp +0 -0
  14. package/public/landing/cats/laptop.webp +0 -0
  15. package/public/landing/cats/map-inspector.webp +0 -0
  16. package/public/landing/cats/server-break.webp +0 -0
  17. package/public/landing/cats/star-mapper.webp +0 -0
  18. package/public/landing/cats/system-builder.webp +0 -0
  19. package/public/landing/cats/thread-tangle.webp +0 -0
  20. package/public/og.png +0 -0
  21. package/public/readme-header.webp +0 -0
  22. package/schema/portolan.schema.json +180 -0
  23. package/scripts/builtin-plugins.mjs +14 -0
  24. package/scripts/delivery-presets.mjs +353 -0
  25. package/scripts/gen.mjs +37 -4
  26. package/scripts/history.mjs +126 -0
  27. package/scripts/history.test.mjs +110 -0
  28. package/scripts/host-plugins/fetch-bsr.mjs +351 -0
  29. package/{plugins/fetch-bsr/options.schema.json → scripts/host-plugins/fetch-bsr.options.json} +4 -0
  30. package/scripts/host-plugins/fetch-bsr.test.mjs +212 -0
  31. package/scripts/host-plugins/fetch-csr.mjs +386 -0
  32. package/scripts/host-plugins/fetch-csr.test.mjs +178 -0
  33. package/scripts/host-plugins/fetch-git.mjs +379 -0
  34. package/scripts/host-plugins/fetch-git.test.mjs +210 -0
  35. package/scripts/local-api.mjs +22 -4
  36. package/scripts/local-api.test.mjs +84 -2
  37. package/scripts/manifest.mjs +3 -2
  38. package/scripts/package-smoke.mjs +6 -2
  39. package/scripts/plugin-host.mjs +77 -18
  40. package/scripts/plugin-host.test.mjs +50 -1
  41. package/scripts/plugin-wasm-worker.mjs +6 -3
  42. package/scripts/run-builtin.mjs +6 -26
  43. package/scripts/schema.mjs +6 -1
  44. package/src/app/App.tsx +24 -429
  45. package/src/app/Breadcrumbs.test.ts +7 -0
  46. package/src/app/Breadcrumbs.tsx +15 -2
  47. package/src/app/CatalogApp.tsx +436 -0
  48. package/src/app/Sidebar.tsx +0 -1
  49. package/src/chat/ChatPanel.tsx +98 -41
  50. package/src/chat/Composer.tsx +4 -2
  51. package/src/chat/Conversation.tsx +79 -15
  52. package/src/chat/Header.tsx +43 -9
  53. package/src/chat/Starter.tsx +64 -3
  54. package/src/chat/Steps.tsx +1 -1
  55. package/src/chat/flags.test.ts +51 -14
  56. package/src/chat/flags.ts +8 -2
  57. package/src/chat/page-context.test.ts +51 -0
  58. package/src/chat/page-context.ts +128 -0
  59. package/src/chat/prompt.test.ts +49 -1
  60. package/src/chat/prompt.ts +42 -1
  61. package/src/chat/tools.ts +7 -2
  62. package/src/chat/transport.ts +16 -4
  63. package/src/components/ProblemRow.tsx +182 -0
  64. package/src/data.ts +20 -5
  65. package/src/er/ErCanvas.tsx +1 -1
  66. package/src/graph/DependencyGraph.tsx +10 -1
  67. package/src/graph/FocusedEventGraph.tsx +1 -1
  68. package/src/graph/GraphToolbar.tsx +2 -4
  69. package/src/index.css +220 -2
  70. package/src/landing/DraggableReveal.tsx +168 -0
  71. package/src/landing/EstateGraph.tsx +28 -0
  72. package/src/landing/FlowPlayback.tsx +282 -0
  73. package/src/landing/HeroMap.tsx +128 -0
  74. package/src/landing/LandingPage.tsx +757 -0
  75. package/src/landing/ProductFrame.tsx +68 -0
  76. package/src/landing/ProductTour.tsx +371 -0
  77. package/src/landing/catalog.ts +25 -0
  78. package/src/landing/motion.tsx +52 -0
  79. package/src/lib/all-problems.ts +28 -0
  80. package/src/lib/local-api.ts +35 -1
  81. package/src/lib/setup-info.test.ts +8 -0
  82. package/src/lib/setup-info.ts +3 -2
  83. package/src/lib/source-code.ts +2 -1
  84. package/src/likec4/C4View.tsx +5 -0
  85. package/src/likec4/InteractiveView.tsx +5 -1
  86. package/src/map/ContextMapGraph.tsx +47 -20
  87. package/src/pages/ContextMap.tsx +5 -1
  88. package/src/pages/FlowDetail.tsx +24 -7
  89. package/src/pages/Overview.tsx +68 -12
  90. package/src/pages/Problems.tsx +6 -193
  91. package/src/pages/Settings.tsx +133 -108
  92. package/src/pages/settings/DeliverySettings.tsx +229 -0
  93. package/src/pages/settings/PreferencesSettings.tsx +101 -0
  94. package/src/routes.test.ts +20 -0
  95. package/src/routes.ts +13 -1
  96. package/vite.config.ts +12 -8
  97. package/catalog/enum_test.go +0 -46
  98. package/catalog/model.go +0 -932
  99. package/catalog/roundtrip_test.go +0 -185
  100. package/catalog/via_test.go +0 -38
  101. package/go.mod +0 -14
  102. package/go.sum +0 -14
  103. package/internal/commands/cargo.go +0 -154
  104. package/internal/commands/commands.go +0 -143
  105. package/internal/commands/commands_test.go +0 -323
  106. package/internal/commands/gradle.go +0 -77
  107. package/internal/commands/justfile.go +0 -82
  108. package/internal/commands/makefile.go +0 -106
  109. package/internal/commands/maven.go +0 -93
  110. package/internal/commands/packagejson.go +0 -159
  111. package/internal/commands/pyproject.go +0 -169
  112. package/internal/commands/taskfile.go +0 -101
  113. package/internal/commands/testdata/estate/.cargo/config.toml +0 -7
  114. package/internal/commands/testdata/estate/Makefile +0 -33
  115. package/internal/commands/testdata/estate/Taskfile.yml +0 -21
  116. package/internal/commands/testdata/estate/build.gradle.kts +0 -21
  117. package/internal/commands/testdata/estate/justfile +0 -20
  118. package/internal/commands/testdata/estate/package.json +0 -13
  119. package/internal/commands/testdata/estate/pom.xml +0 -29
  120. package/internal/commands/testdata/estate/pyproject.toml +0 -27
  121. package/internal/commands/testdata/estate/xtask/src/main.rs +0 -27
  122. package/internal/commands/testdata/golden/commands.json +0 -305
  123. package/internal/gohttp/analyze.go +0 -2500
  124. package/internal/gohttp/endpoints.go +0 -1067
  125. package/internal/gohttp/roots.go +0 -320
  126. package/internal/gohttp/typed.go +0 -143
  127. package/internal/goscan/constants.go +0 -85
  128. package/internal/goscan/goscan_test.go +0 -227
  129. package/internal/goscan/names.go +0 -52
  130. package/internal/goscan/parse_test.go +0 -11
  131. package/internal/goscan/source.go +0 -36
  132. package/internal/goscan/tree.go +0 -155
  133. package/internal/goscan/types.go +0 -99
  134. package/internal/wsdl/ids.go +0 -127
  135. package/internal/wsdl/ids_test.go +0 -21
  136. package/internal/wsdl/model.go +0 -62
  137. package/internal/wsdl/parse.go +0 -920
  138. package/internal/wsdl/parse_test.go +0 -133
  139. package/plugin/describe.go +0 -107
  140. package/plugin/describe_test.go +0 -114
  141. package/plugin/protocol.go +0 -117
  142. package/plugin/schematest/schematest.go +0 -126
  143. package/plugins/extract-adr/describe.go +0 -19
  144. package/plugins/extract-adr/describe_test.go +0 -11
  145. package/plugins/extract-adr/extract.go +0 -153
  146. package/plugins/extract-adr/extract_test.go +0 -350
  147. package/plugins/extract-adr/history.go +0 -99
  148. package/plugins/extract-adr/main.go +0 -65
  149. package/plugins/extract-adr/parse.go +0 -642
  150. package/plugins/extract-adr/parse_test.go +0 -405
  151. package/plugins/extract-asyncapi/describe.go +0 -19
  152. package/plugins/extract-asyncapi/describe_test.go +0 -11
  153. package/plugins/extract-asyncapi/extract.go +0 -315
  154. package/plugins/extract-asyncapi/extract_test.go +0 -197
  155. package/plugins/extract-asyncapi/main.go +0 -48
  156. package/plugins/extract-asyncapi/spec.go +0 -151
  157. package/plugins/extract-commands/describe.go +0 -19
  158. package/plugins/extract-commands/describe_test.go +0 -11
  159. package/plugins/extract-commands/extract.go +0 -79
  160. package/plugins/extract-commands/extract_test.go +0 -92
  161. package/plugins/extract-commands/main.go +0 -55
  162. package/plugins/extract-csr/avro.go +0 -258
  163. package/plugins/extract-csr/describe.go +0 -19
  164. package/plugins/extract-csr/describe_test.go +0 -11
  165. package/plugins/extract-csr/extract.go +0 -338
  166. package/plugins/extract-csr/extract_test.go +0 -374
  167. package/plugins/extract-csr/jsonschema.go +0 -343
  168. package/plugins/extract-csr/lock.go +0 -25
  169. package/plugins/extract-csr/main.go +0 -84
  170. package/plugins/extract-csr/subject.go +0 -102
  171. package/plugins/extract-flows/describe.go +0 -19
  172. package/plugins/extract-flows/describe_test.go +0 -11
  173. package/plugins/extract-flows/extract.go +0 -91
  174. package/plugins/extract-flows/main.go +0 -45
  175. package/plugins/extract-flows/parse.go +0 -593
  176. package/plugins/extract-flows/parse_test.go +0 -204
  177. package/plugins/extract-glossary/describe.go +0 -19
  178. package/plugins/extract-glossary/describe_test.go +0 -11
  179. package/plugins/extract-glossary/extract.go +0 -115
  180. package/plugins/extract-glossary/extract_test.go +0 -220
  181. package/plugins/extract-glossary/main.go +0 -59
  182. package/plugins/extract-glossary/parse.go +0 -214
  183. package/plugins/extract-glossary/parse_test.go +0 -203
  184. package/plugins/extract-go/aggregate.go +0 -214
  185. package/plugins/extract-go/client.go +0 -409
  186. package/plugins/extract-go/client_test.go +0 -266
  187. package/plugins/extract-go/describe.go +0 -19
  188. package/plugins/extract-go/describe_test.go +0 -11
  189. package/plugins/extract-go/enum.go +0 -195
  190. package/plugins/extract-go/enum_test.go +0 -82
  191. package/plugins/extract-go/event.go +0 -99
  192. package/plugins/extract-go/extract.go +0 -191
  193. package/plugins/extract-go/extract_test.go +0 -261
  194. package/plugins/extract-go/flow.go +0 -1441
  195. package/plugins/extract-go/flow_test.go +0 -609
  196. package/plugins/extract-go/httpclient.go +0 -174
  197. package/plugins/extract-go/httpclient_test.go +0 -296
  198. package/plugins/extract-go/ids.go +0 -92
  199. package/plugins/extract-go/layout.go +0 -225
  200. package/plugins/extract-go/layout_test.go +0 -125
  201. package/plugins/extract-go/lifecycle.go +0 -324
  202. package/plugins/extract-go/lifecycle_test.go +0 -108
  203. package/plugins/extract-go/main.go +0 -79
  204. package/plugins/extract-go/operation.go +0 -157
  205. package/plugins/extract-go/source.go +0 -316
  206. package/plugins/extract-go/transport.go +0 -321
  207. package/plugins/extract-go/transport_test.go +0 -145
  208. package/plugins/extract-go/wiring.go +0 -434
  209. package/plugins/extract-go-nats/describe.go +0 -19
  210. package/plugins/extract-go-nats/describe_test.go +0 -11
  211. package/plugins/extract-go-nats/extract.go +0 -177
  212. package/plugins/extract-go-nats/extract_test.go +0 -389
  213. package/plugins/extract-go-nats/index.go +0 -379
  214. package/plugins/extract-go-nats/main.go +0 -42
  215. package/plugins/extract-go-nats/resolve.go +0 -161
  216. package/plugins/extract-go-nats/sites.go +0 -224
  217. package/plugins/extract-graphql/describe.go +0 -19
  218. package/plugins/extract-graphql/describe_test.go +0 -11
  219. package/plugins/extract-graphql/extract.go +0 -433
  220. package/plugins/extract-graphql/extract_test.go +0 -256
  221. package/plugins/extract-graphql/ids.go +0 -49
  222. package/plugins/extract-graphql/lex.go +0 -237
  223. package/plugins/extract-graphql/main.go +0 -51
  224. package/plugins/extract-graphql/parse.go +0 -621
  225. package/plugins/extract-graphql/parse_test.go +0 -122
  226. package/plugins/extract-http-clients/describe.go +0 -19
  227. package/plugins/extract-http-clients/describe_test.go +0 -11
  228. package/plugins/extract-http-clients/extract.go +0 -705
  229. package/plugins/extract-http-clients/extract_test.go +0 -1263
  230. package/plugins/extract-http-clients/main.go +0 -42
  231. package/plugins/extract-openapi/describe.go +0 -19
  232. package/plugins/extract-openapi/describe_test.go +0 -11
  233. package/plugins/extract-openapi/discover.go +0 -243
  234. package/plugins/extract-openapi/extract.go +0 -526
  235. package/plugins/extract-openapi/extract_test.go +0 -545
  236. package/plugins/extract-openapi/main.go +0 -75
  237. package/plugins/extract-openapi/spec.go +0 -350
  238. package/plugins/extract-project/describe.go +0 -19
  239. package/plugins/extract-project/describe_test.go +0 -11
  240. package/plugins/extract-project/extract.go +0 -221
  241. package/plugins/extract-project/extract_test.go +0 -109
  242. package/plugins/extract-project/main.go +0 -41
  243. package/plugins/extract-proto/ast.go +0 -125
  244. package/plugins/extract-proto/consumes.go +0 -77
  245. package/plugins/extract-proto/describe.go +0 -19
  246. package/plugins/extract-proto/extract.go +0 -293
  247. package/plugins/extract-proto/extract_test.go +0 -459
  248. package/plugins/extract-proto/ids.go +0 -89
  249. package/plugins/extract-proto/ids_test.go +0 -57
  250. package/plugins/extract-proto/lex.go +0 -285
  251. package/plugins/extract-proto/main.go +0 -100
  252. package/plugins/extract-proto/module.go +0 -120
  253. package/plugins/extract-proto/parse.go +0 -720
  254. package/plugins/extract-proto/parse_test.go +0 -307
  255. package/plugins/extract-proto/provides.go +0 -236
  256. package/plugins/extract-proto/resolve.go +0 -222
  257. package/plugins/extract-proto/resolve_test.go +0 -119
  258. package/plugins/extract-redis/describe.go +0 -19
  259. package/plugins/extract-redis/describe_test.go +0 -11
  260. package/plugins/extract-redis/extract.go +0 -183
  261. package/plugins/extract-redis/extract_test.go +0 -168
  262. package/plugins/extract-redis/keyspaces.go +0 -469
  263. package/plugins/extract-redis/main.go +0 -44
  264. package/plugins/extract-river/describe.go +0 -19
  265. package/plugins/extract-river/describe_test.go +0 -11
  266. package/plugins/extract-river/extract.go +0 -507
  267. package/plugins/extract-river/extract_test.go +0 -132
  268. package/plugins/extract-river/main.go +0 -42
  269. package/plugins/extract-sql/ddl.go +0 -893
  270. package/plugins/extract-sql/ddl_test.go +0 -401
  271. package/plugins/extract-sql/describe.go +0 -19
  272. package/plugins/extract-sql/describe_test.go +0 -11
  273. package/plugins/extract-sql/layout.go +0 -221
  274. package/plugins/extract-sql/layout_test.go +0 -55
  275. package/plugins/extract-sql/lineage.go +0 -107
  276. package/plugins/extract-sql/main.go +0 -141
  277. package/plugins/extract-sql/maps.go +0 -564
  278. package/plugins/extract-sql/maps_java.go +0 -117
  279. package/plugins/extract-sql/maps_rust.go +0 -333
  280. package/plugins/extract-sql/maps_rust_test.go +0 -70
  281. package/plugins/extract-sql/maps_test.go +0 -204
  282. package/plugins/extract-sql/maps_ts.go +0 -398
  283. package/plugins/extract-sql/maps_ts_test.go +0 -136
  284. package/plugins/extract-sql/projection.go +0 -70
  285. package/plugins/extract-sql/projection_test.go +0 -50
  286. package/plugins/extract-sql/store.go +0 -420
  287. package/plugins/extract-sql/store_test.go +0 -233
  288. package/plugins/extract-sql/view.go +0 -295
  289. package/plugins/extract-watermill/describe.go +0 -19
  290. package/plugins/extract-watermill/describe_test.go +0 -11
  291. package/plugins/extract-watermill/extract.go +0 -1228
  292. package/plugins/extract-watermill/extract_test.go +0 -234
  293. package/plugins/extract-watermill/main.go +0 -41
  294. package/plugins/extract-wsdl/describe.go +0 -19
  295. package/plugins/extract-wsdl/describe_test.go +0 -11
  296. package/plugins/extract-wsdl/extract.go +0 -148
  297. package/plugins/extract-wsdl/extract_test.go +0 -45
  298. package/plugins/extract-wsdl/main.go +0 -53
  299. package/plugins/fetch-bsr/auth.go +0 -114
  300. package/plugins/fetch-bsr/bsr.go +0 -240
  301. package/plugins/fetch-bsr/cache.go +0 -66
  302. package/plugins/fetch-bsr/describe.go +0 -19
  303. package/plugins/fetch-bsr/fetch.go +0 -192
  304. package/plugins/fetch-bsr/fetch_test.go +0 -465
  305. package/plugins/fetch-bsr/lock.go +0 -71
  306. package/plugins/fetch-bsr/main.go +0 -117
  307. package/plugins/fetch-csr/auth.go +0 -95
  308. package/plugins/fetch-csr/cache.go +0 -69
  309. package/plugins/fetch-csr/describe.go +0 -19
  310. package/plugins/fetch-csr/describe_test.go +0 -11
  311. package/plugins/fetch-csr/fetch.go +0 -254
  312. package/plugins/fetch-csr/fetch_test.go +0 -494
  313. package/plugins/fetch-csr/lock.go +0 -95
  314. package/plugins/fetch-csr/main.go +0 -122
  315. package/plugins/fetch-csr/registry.go +0 -209
  316. package/plugins/fetch-git/cache.go +0 -65
  317. package/plugins/fetch-git/describe.go +0 -19
  318. package/plugins/fetch-git/describe_test.go +0 -11
  319. package/plugins/fetch-git/fetch.go +0 -181
  320. package/plugins/fetch-git/fetch_test.go +0 -332
  321. package/plugins/fetch-git/git.go +0 -169
  322. package/plugins/fetch-git/lock.go +0 -72
  323. package/plugins/fetch-git/main.go +0 -127
  324. package/plugins/fetch-git/offline.go +0 -39
  325. package/plugins/fetch-git/pin.go +0 -90
  326. package/plugins/fetch-git/pin_test.go +0 -74
  327. package/plugins/gen-backstage/describe.go +0 -17
  328. package/plugins/gen-backstage/main.go +0 -22
  329. package/plugins/gen-backstage/plugin.go +0 -473
  330. package/plugins/gen-backstage/plugin_test.go +0 -145
  331. package/plugins/gen-backstage.wasm +0 -0
  332. package/plugins/gen-markdown/adr.go +0 -162
  333. package/plugins/gen-markdown/aggregate.go +0 -362
  334. package/plugins/gen-markdown/canonical.go +0 -93
  335. package/plugins/gen-markdown/context.go +0 -90
  336. package/plugins/gen-markdown/coverage_test.go +0 -89
  337. package/plugins/gen-markdown/describe.go +0 -19
  338. package/plugins/gen-markdown/describe_test.go +0 -11
  339. package/plugins/gen-markdown/external.go +0 -71
  340. package/plugins/gen-markdown/flow.go +0 -277
  341. package/plugins/gen-markdown/glossary.go +0 -76
  342. package/plugins/gen-markdown/glossary_test.go +0 -148
  343. package/plugins/gen-markdown/llms.go +0 -302
  344. package/plugins/gen-markdown/main.go +0 -22
  345. package/plugins/gen-markdown/markdown.go +0 -254
  346. package/plugins/gen-markdown/markdown_test.go +0 -100
  347. package/plugins/gen-markdown/module.go +0 -107
  348. package/plugins/gen-markdown/plugin.go +0 -39
  349. package/plugins/gen-markdown/quality_test.go +0 -176
  350. package/plugins/gen-markdown/redis_test.go +0 -38
  351. package/plugins/gen-markdown/render.go +0 -344
  352. package/plugins/gen-markdown/render_test.go +0 -186
  353. package/plugins/gen-markdown/service.go +0 -456
  354. package/plugins/gen-markdown/source.go +0 -177
  355. package/plugins/gen-markdown/store.go +0 -201
  356. package/plugins/gen-markdown.wasm +0 -0
  357. package/plugins/gen-mermaid/describe.go +0 -17
  358. package/plugins/gen-mermaid/main.go +0 -22
  359. package/plugins/gen-mermaid/plugin.go +0 -104
  360. package/plugins/gen-mermaid/plugin_test.go +0 -33
  361. package/plugins/gen-mermaid.wasm +0 -0
  362. package/plugins/openapi/ids.go +0 -261
  363. package/plugins/openapi/ids_test.go +0 -98
  364. package/plugins/verify-codeowners/describe.go +0 -19
  365. package/plugins/verify-codeowners/describe_test.go +0 -11
  366. package/plugins/verify-codeowners/main.go +0 -75
  367. package/plugins/verify-codeowners/match.go +0 -85
  368. package/plugins/verify-codeowners/match_test.go +0 -47
  369. package/plugins/verify-codeowners/owners.go +0 -164
  370. package/plugins/verify-codeowners/owners_test.go +0 -225
  371. package/plugins/verify-codeowners/parse.go +0 -90
  372. package/plugins/verify-codeowners/parse_test.go +0 -62
  373. package/plugins/verify-otel/describe.go +0 -19
  374. package/plugins/verify-otel/describe_test.go +0 -11
  375. package/plugins/verify-otel/main.go +0 -53
  376. package/plugins/verify-otel/match.go +0 -336
  377. package/plugins/verify-otel/otlp.go +0 -200
  378. package/plugins/verify-otel/verify.go +0 -734
  379. package/plugins/verify-otel/verify_test.go +0 -460
  380. /package/{plugins/fetch-csr/options.schema.json → scripts/host-plugins/fetch-csr.options.json} +0 -0
  381. /package/{plugins/fetch-git/options.schema.json → scripts/host-plugins/fetch-git.options.json} +0 -0
@@ -1,398 +0,0 @@
1
- package main
2
-
3
- import (
4
- "os"
5
- "path/filepath"
6
- "regexp"
7
- "strings"
8
-
9
- "github.com/pgplex/pgparser/nodes"
10
- pgsql "github.com/pgplex/pgparser/parser"
11
- "github.com/shortlink-org/portolan/plugin"
12
- )
13
-
14
- // The same fact read out of a repository written in TypeScript, where the
15
- // statement and its arguments sit side by side in one call:
16
- //
17
- // client.query(
18
- // "INSERT INTO baskets (id, token, customer_id, touched_at) VALUES ($1, $2, $3, $4)",
19
- // [basket.id, basket.token, basket.customerId, basket.touchedAt.toISOString()],
20
- // )
21
- //
22
- // There is no parser for the language here, and none is needed for this
23
- // shape: the statement is a string literal, the arguments are an array
24
- // literal right after it, and each argument is a property chain from the
25
- // aggregate - possibly wrapped in one conversion. Anything else is left
26
- // unmapped, for the same reason as in the Go reader: a blank is a reader
27
- // looking the column up, a wrong one is a reader believing something untrue.
28
- func readMapsTS(root, repositoryDir, aggregate string, b *plugin.Builder) map[string]map[string]string {
29
- out := map[string]map[string]string{}
30
- dir := mapSourceDir(root, repositoryDir, aggregate)
31
- entries, err := os.ReadDir(filepath.Join(root, filepath.FromSlash(dir)))
32
- if err != nil {
33
- return out
34
- }
35
- root_ := title(aggregate)
36
-
37
- for _, entry := range entries {
38
- name := entry.Name()
39
- if entry.IsDir() || !strings.HasSuffix(name, ".ts") || strings.HasSuffix(name, ".test.ts") || strings.HasSuffix(name, ".d.ts") {
40
- continue
41
- }
42
- source, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(dir), name))
43
- if err != nil {
44
- continue
45
- }
46
- roots := rootVarsTS(string(source), root_)
47
- aliases := loopAliasesTS(string(source))
48
- for _, stmt := range insertCalls(string(source)) {
49
- table, columns, values := parseInsert(stmt.sql)
50
- if table == "" {
51
- continue
52
- }
53
- mapped := out[table]
54
- if mapped == nil {
55
- mapped = map[string]string{}
56
- out[table] = mapped
57
- }
58
- numbers := placeholderNumbers(stmt.sql)
59
- if numbers == nil || countParams(values) != len(numbers) {
60
- if countParams(values) > 0 {
61
- b.Warn(table, "the insert into "+table+" has placeholders this reader cannot number; its columns are left unmapped")
62
- }
63
- continue
64
- }
65
- seen := 0
66
- for i, column := range columns {
67
- if i >= len(values) {
68
- break
69
- }
70
- if _, ok := values[i].(*nodes.ParamRef); !ok {
71
- continue
72
- }
73
- number := numbers[seen]
74
- seen++
75
- if number < 1 || number > len(stmt.args) {
76
- b.Warn(table, "the insert refers to $"+itoa(number)+" but the call passes "+itoa(len(stmt.args))+" values; "+column+" is left unmapped")
77
- continue
78
- }
79
- field := fieldOfTS(stmt.args[number-1], roots, aliases)
80
- if field == "" {
81
- continue
82
- }
83
- mapped[column] = root_ + "." + field
84
- }
85
- }
86
- }
87
-
88
- return out
89
- }
90
-
91
- // tsInsert is one statement and the array of arguments written after it.
92
- type tsInsert struct {
93
- sql string
94
- args []string
95
- }
96
-
97
- // insertCalls walks the source one character at a time, because a string
98
- // literal can only be found by reading from its opening quote to its closing
99
- // one: a pattern that skips ahead to the next quote it likes will start in
100
- // one literal and end in another, and read the code between them as SQL.
101
- // Comments are stepped over the same way. A literal that holds an INSERT and
102
- // is followed by a comma and an array is a statement with its arguments.
103
- func insertCalls(source string) []tsInsert {
104
- var out []tsInsert
105
- for i := 0; i < len(source); i++ {
106
- c := source[i]
107
- switch {
108
- case c == '/' && i+1 < len(source) && source[i+1] == '/':
109
- for i < len(source) && source[i] != '\n' {
110
- i++
111
- }
112
- case c == '/' && i+1 < len(source) && source[i+1] == '*':
113
- end := strings.Index(source[i+2:], "*/")
114
- if end < 0 {
115
- return out
116
- }
117
- i += 2 + end + 1
118
- case c == '"' || c == '\'' || c == '`':
119
- end := closingQuote(source, i)
120
- if end < 0 {
121
- return out
122
- }
123
- sql := source[i+1 : end]
124
- i = end
125
- j := skipSpace(source, end+1)
126
- if j >= len(source) || source[j] != ',' {
127
- continue
128
- }
129
- j = skipSpace(source, j+1)
130
- if j >= len(source) || source[j] != '[' || !strings.Contains(strings.ToUpper(sql), "INSERT INTO") {
131
- continue
132
- }
133
- close := matchingBracket(source, j)
134
- if close < 0 {
135
- continue
136
- }
137
- out = append(out, tsInsert{sql: sql, args: splitTopLevel(source[j+1 : close])})
138
- i = close
139
- }
140
- }
141
-
142
- return out
143
- }
144
-
145
- // closingQuote finds the quote that closes the one at open, stepping over
146
- // escapes.
147
- func closingQuote(source string, open int) int {
148
- quote := source[open]
149
- for i := open + 1; i < len(source); i++ {
150
- switch source[i] {
151
- case '\\':
152
- i++
153
- case quote:
154
- return i
155
- }
156
- }
157
-
158
- return -1
159
- }
160
-
161
- func skipSpace(source string, i int) int {
162
- for i < len(source) && (source[i] == ' ' || source[i] == '\t' || source[i] == '\n' || source[i] == '\r') {
163
- i++
164
- }
165
-
166
- return i
167
- }
168
-
169
- // matchingBracket finds the `]` that closes the `[` at open, stepping over
170
- // nested brackets, parentheses, braces and string literals.
171
- func matchingBracket(source string, open int) int {
172
- depth := 0
173
- var quote byte
174
- for i := open; i < len(source); i++ {
175
- c := source[i]
176
- if quote != 0 {
177
- if c == '\\' {
178
- i++
179
- } else if c == quote {
180
- quote = 0
181
- }
182
- continue
183
- }
184
- switch c {
185
- case '"', '\'', '`':
186
- quote = c
187
- case '[', '(', '{':
188
- depth++
189
- case ']', ')', '}':
190
- depth--
191
- if depth == 0 {
192
- return i
193
- }
194
- }
195
- }
196
-
197
- return -1
198
- }
199
-
200
- // splitTopLevel splits an argument list on the commas that are not inside a
201
- // call, an object, an array or a string.
202
- func splitTopLevel(list string) []string {
203
- var out []string
204
- depth := 0
205
- var quote byte
206
- start := 0
207
- for i := 0; i < len(list); i++ {
208
- c := list[i]
209
- if quote != 0 {
210
- if c == '\\' {
211
- i++
212
- } else if c == quote {
213
- quote = 0
214
- }
215
- continue
216
- }
217
- switch c {
218
- case '"', '\'', '`':
219
- quote = c
220
- case '[', '(', '{':
221
- depth++
222
- case ']', ')', '}':
223
- depth--
224
- case ',':
225
- if depth == 0 {
226
- out = append(out, strings.TrimSpace(list[start:i]))
227
- start = i + 1
228
- }
229
- }
230
- }
231
- if last := strings.TrimSpace(list[start:]); last != "" {
232
- out = append(out, last)
233
- }
234
-
235
- return out
236
- }
237
-
238
- var tsChain = regexp.MustCompile(`^[A-Za-z_$][\w$]*(?:\??\.[A-Za-z_$][\w$]*)+$`)
239
-
240
- // fieldOfTS follows an argument back to the field it carries, the way fieldOf
241
- // does for Go: `basket.customerId` is customerId, `basket.touchedAt.toISOString()`
242
- // is touchedAt, `nullable(basket.checkedOutAt)` is checkedOutAt. A bare name,
243
- // a literal, a call that makes its own value - `randomUUID()`, `new Date()` -
244
- // and a call over several values carry no field.
245
- func fieldOfTS(expr string, roots map[string]bool, aliases map[string]string) string {
246
- expr = strings.TrimSpace(expr)
247
- // `basket.customerId ?? null` writes the field with a default for its
248
- // absence; the column still carries the field.
249
- if at := strings.LastIndex(expr, "??"); at >= 0 && tsLiteral.MatchString(strings.TrimSpace(expr[at+2:])) {
250
- expr = strings.TrimSpace(expr[:at])
251
- }
252
- for strings.HasSuffix(expr, ")") {
253
- open := openingParen(expr)
254
- if open <= 0 {
255
- return ""
256
- }
257
- callee, inner := expr[:open], expr[open+1:len(expr)-1]
258
- args := splitTopLevel(inner)
259
- switch {
260
- case tsChain.MatchString(callee) && (len(args) == 0 || tsChain.MatchString(strings.TrimSuffix(callee[:strings.LastIndex(callee, ".")], "?"))):
261
- // A method on the value: the value is the chain before it. With
262
- // no arguments it is a conversion of the value - toISOString() -
263
- // and with some it is still the value's own method when what it
264
- // stands on is itself a chain: basket.items.slice(1) walks the
265
- // items, where JSON.stringify(x) stands on a bare name and is
266
- // the conversion read below.
267
- expr = callee[:strings.LastIndex(callee, ".")]
268
- expr = strings.TrimSuffix(expr, "?")
269
- case len(args) == 1:
270
- // A conversion wrapped round the value.
271
- expr = strings.TrimSpace(args[0])
272
- default:
273
- return ""
274
- }
275
- }
276
- if !tsChain.MatchString(expr) {
277
- return ""
278
- }
279
- parts := strings.Split(strings.ReplaceAll(expr, "?.", "."), ".")
280
- if over, ok := aliases[parts[0]]; ok {
281
- // The variable of a loop stands for one element of what it walks:
282
- // `for (const item of basket.items)` makes item.sku items.sku. What
283
- // is walked is read as an argument is, so a slice of the items is
284
- // still the items. The alias is forgotten for the rest of the read,
285
- // so that a loop written over its own name cannot stand for itself.
286
- rest := make(map[string]string, len(aliases))
287
- for name, chain := range aliases {
288
- if name != parts[0] {
289
- rest[name] = chain
290
- }
291
- }
292
- head := fieldOfTS(over, roots, rest)
293
- if head == "" {
294
- return ""
295
- }
296
-
297
- return strings.Join(append([]string{head}, parts[1:]...), ".")
298
- }
299
- if roots != nil && !roots[parts[0]] {
300
- return ""
301
- }
302
-
303
- return strings.Join(parts[1:], ".")
304
- }
305
-
306
- // tsForOf is the head of a for-of loop, up to what it walks:
307
- // `for (const item of `. What follows runs to the parenthesis that closes
308
- // the loop's own, found by matching rather than by pattern because a slice
309
- // or a filter carries parentheses of its own.
310
- var tsForOf = regexp.MustCompile(`for\s*\(\s*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s+of\s+`)
311
-
312
- // loopAliasesTS reads every for-of loop of the file: the variable, and the
313
- // expression it walks.
314
- func loopAliasesTS(source string) map[string]string {
315
- var aliases map[string]string
316
- for _, m := range tsForOf.FindAllStringSubmatchIndex(source, -1) {
317
- open := strings.IndexByte(source[m[0]:], '(') + m[0]
318
- close := matchingBracket(source, open)
319
- if close < 0 || close < m[1] {
320
- continue
321
- }
322
- if aliases == nil {
323
- aliases = map[string]string{}
324
- }
325
- aliases[source[m[2]:m[3]]] = strings.TrimSpace(source[m[1]:close])
326
- }
327
-
328
- return aliases
329
- }
330
-
331
- // tsLiteral is what may stand after `??` without changing which field the
332
- // argument carries: nothing, or a constant.
333
- var tsLiteral = regexp.MustCompile(`^(null|undefined|true|false|-?\d+(\.\d+)?|"[^"]*"|'[^']*')$`)
334
-
335
- // tsParam is a name declared with the type after it: a parameter, a field, a
336
- // variable. `route: Route` is the aggregate wherever it appears in the file.
337
- var tsParam = regexp.MustCompile(`\b([A-Za-z_$][\w$]*)\??\s*:\s*([A-Za-z_$][\w$]*)\b`)
338
-
339
- // rootVarsTS names the variables the file declares with the root's type, or
340
- // nil when it declares none. Inside save(route: Route, ...) the aggregate is
341
- // route and nothing else: the loop variable over its stops carries a stop's
342
- // fields, and a column mapped from it onto the root would be a claim about
343
- // the wrong type. A file that never names the type - a repository over a row
344
- // type of its own - is read as before, any bare name standing for the root.
345
- func rootVarsTS(source, root string) map[string]bool {
346
- var roots map[string]bool
347
- for _, m := range tsParam.FindAllStringSubmatch(source, -1) {
348
- if m[2] != root {
349
- continue
350
- }
351
- if roots == nil {
352
- roots = map[string]bool{}
353
- }
354
- roots[m[1]] = true
355
- }
356
-
357
- return roots
358
- }
359
-
360
- // openingParen finds the `(` that the closing paren at the end of expr pairs
361
- // with.
362
- func openingParen(expr string) int {
363
- depth := 0
364
- for i := len(expr) - 1; i >= 0; i-- {
365
- switch expr[i] {
366
- case ')':
367
- depth++
368
- case '(':
369
- depth--
370
- if depth == 0 {
371
- return i
372
- }
373
- }
374
- }
375
-
376
- return -1
377
- }
378
-
379
- // parseInsert reads the table, its column list and its values out of one
380
- // INSERT statement.
381
- func parseInsert(sql string) (string, []string, []nodes.Node) {
382
- tree, err := pgsql.Parse(sql)
383
- if err != nil || len(tree.Items) == 0 {
384
- return "", nil, nil
385
- }
386
- insert, ok := tree.Items[0].(*nodes.InsertStmt)
387
- if !ok || insert.Relation == nil {
388
- return "", nil, nil
389
- }
390
- var columns []string
391
- for _, item := range items(insert.Cols) {
392
- if target, ok := item.(*nodes.ResTarget); ok {
393
- columns = append(columns, target.Name)
394
- }
395
- }
396
-
397
- return relationName(insert.Relation), columns, insertValues(insert)
398
- }
@@ -1,136 +0,0 @@
1
- package main
2
-
3
- import (
4
- "testing"
5
- )
6
-
7
- // The same rule as for Go, read off text: the chain from the aggregate, one
8
- // conversion allowed round it, a method on the value stripped.
9
- func TestFieldOfTSFollowsTheChainAndOneWrapper(t *testing.T) {
10
- cases := map[string]string{
11
- "basket.id": "id",
12
- "basket.customerId": "customerId",
13
- "basket.touchedAt.toISOString()": "touchedAt",
14
- "basket.unitPrice?.amountMinor": "unitPrice.amountMinor",
15
- "nullable(basket.checkedOutAt)": "checkedOutAt",
16
- "String(basket.version)": "version",
17
- "JSON.stringify(toWire(event))": "",
18
- "randomUUID()": "",
19
- "new Date()": "",
20
- "combine(basket.a, basket.b)": "",
21
- "TOPIC": "",
22
- "'literal'": "",
23
- "basket.items.slice(1)": "items",
24
- "JSON.stringify(basket.shipTo)": "shipTo",
25
- "basket.customerId ?? null": "customerId",
26
- "basket.currency?.code ?? null": "currency.code",
27
- "basket.version ?? 0": "version",
28
- "basket.a ?? basket.b": "",
29
- }
30
- for in, want := range cases {
31
- if got := fieldOfTS(in, nil, nil); got != want {
32
- t.Errorf("fieldOfTS(%q) = %q, want %q", in, got, want)
33
- }
34
- }
35
- }
36
-
37
- // Once the file says which name is the aggregate, only that name maps: the
38
- // item of a loop over its lines is not the basket.
39
- func TestFieldOfTSReadsOnlyTheRootWhenTheFileNamesIt(t *testing.T) {
40
- roots := rootVarsTS("async save(basket: Basket, ...events: BasketEvent[]) { for (const item of basket.items) {} }", "Basket")
41
- if !roots["basket"] || roots["events"] || roots["item"] {
42
- t.Fatalf("roots = %v", roots)
43
- }
44
- cases := map[string]string{
45
- "basket.id": "id",
46
- "item.sku": "",
47
- "item.quantity": "",
48
- }
49
- for in, want := range cases {
50
- if got := fieldOfTS(in, roots, nil); got != want {
51
- t.Errorf("fieldOfTS(%q) = %q, want %q", in, got, want)
52
- }
53
- }
54
- if rootVarsTS("const rows = await pool.query<Row>(sql)", "Basket") != nil {
55
- t.Error("a file that never names the root should leave every name open")
56
- }
57
- }
58
-
59
- // The variable of a for-of loop stands for one element of what it walks, so
60
- // a column written from it carries a field of the elements of a field.
61
- func TestFieldOfTSFollowsALoopVariable(t *testing.T) {
62
- aliases := loopAliasesTS(`for (const item of basket.items) { for (const event of events) {} } for (const self of self.items) {}
63
- for (const scan of basket.scans.slice(stored.rows[0]?.n ?? 0)) {}`)
64
- if aliases["item"] != "basket.items" || aliases["event"] != "events" || aliases["self"] != "self.items" || aliases["scan"] != "basket.scans.slice(stored.rows[0]?.n ?? 0)" {
65
- t.Fatalf("aliases = %v", aliases)
66
- }
67
- roots := map[string]bool{"basket": true}
68
- cases := map[string]string{
69
- "item.sku": "items.sku",
70
- "item.unitPrice.currency.code": "items.unitPrice.currency.code",
71
- "item.touchedAt.toISOString()": "items.touchedAt",
72
- "scan.parcelId": "scans.parcelId",
73
- "event.name": "",
74
- "self.name": "",
75
- }
76
- for in, want := range cases {
77
- if got := fieldOfTS(in, roots, aliases); got != want {
78
- t.Errorf("fieldOfTS(%q) = %q, want %q", in, got, want)
79
- }
80
- }
81
- }
82
-
83
- func TestMapsTSAgainstTheRealService(t *testing.T) {
84
- b := builderFor(t)
85
- mapped := readMapsTS("../../examples/shop/cart", "src/infrastructure/repository/basket", "basket", b)
86
-
87
- want := map[string]map[string]string{
88
- "baskets": {
89
- "id": "Basket.id",
90
- "token": "Basket.token",
91
- "customer_id": "Basket.customerId",
92
- "currency": "Basket.currency.code",
93
- "status": "Basket.status",
94
- "touched_at": "Basket.touchedAt",
95
- },
96
- "basket_items": {
97
- "basket_id": "Basket.id",
98
- "sku": "Basket.items.sku",
99
- "quantity": "Basket.items.quantity",
100
- "unit_price_minor": "Basket.items.unitPrice.amountMinor",
101
- "currency": "Basket.items.unitPrice.currency.code",
102
- },
103
- }
104
- for table, columns := range want {
105
- for column, field := range columns {
106
- if got := mapped[table][column]; got != field {
107
- t.Errorf("%s.%s -> %q, want %q", table, column, got, field)
108
- }
109
- }
110
- if len(mapped[table]) != len(columns) {
111
- t.Errorf("%s: mapped %v, want exactly %v", table, mapped[table], columns)
112
- }
113
- }
114
- }
115
-
116
- func TestInsertCallsPairAStatementWithItsArray(t *testing.T) {
117
- source := `
118
- await client.query("BEGIN"); // a literal before, which must not swallow what follows
119
- await client.query(
120
- "INSERT INTO baskets (id, token, version) VALUES ($1, $2, 1)",
121
- [basket.id, basket.token],
122
- );
123
- const rows = await this.pool.query<Row>("SELECT * FROM baskets WHERE id = $1", [id]);
124
- await client.query("INSERT INTO outbox (uuid, payload) VALUES ($1, $2)", [randomUUID(), JSON.stringify({ a: [1, 2] })]);
125
- `
126
- calls := insertCalls(source)
127
- if len(calls) != 2 {
128
- t.Fatalf("found %d inserts, want 2", len(calls))
129
- }
130
- if len(calls[0].args) != 2 || calls[0].args[1] != "basket.token" {
131
- t.Fatalf("first insert args = %q", calls[0].args)
132
- }
133
- if len(calls[1].args) != 2 || calls[1].args[1] != "JSON.stringify({ a: [1, 2] })" {
134
- t.Fatalf("second insert args = %q", calls[1].args)
135
- }
136
- }
@@ -1,70 +0,0 @@
1
- package main
2
-
3
- // Whose rows a projection is a picture of, when the migration says.
4
- //
5
- // A repository package names its aggregate by where it sits: the directory is
6
- // the aggregate, and the first table it creates is the root. A projector
7
- // package cannot say it that way. The projection is named after the question
8
- // it answers, not after an aggregate, and the aggregate it is built from may
9
- // belong to another service whose name appears nowhere in this tree.
10
- //
11
- // So it is declared, in the same form a copied column already uses, above the
12
- // CREATE TABLE it speaks for:
13
- //
14
- // -- Planned stops, rebuilt from RoutePlanned.
15
- // -- aggregate: shop.oms.order
16
- // CREATE TABLE route_stops (
17
- //
18
- // A bare slug (`route`) is an aggregate of the service that owns the store and
19
- // is spelled the way the repository layout would spell it: `price_list` is
20
- // `price-list`. Anything with a dot in it is a full id and is taken as
21
- // written. Other comment lines may sit between the note and the statement; a
22
- // statement may not, and a note that reaches nothing is dropped.
23
-
24
- import (
25
- "regexp"
26
- "strings"
27
- )
28
-
29
- var aggregateComment = regexp.MustCompile(`(?i)^\s*--\s*aggregate:\s*(.+?)\s*$`)
30
-
31
- // readProjected reads the `-- aggregate:` lines of one migration, as
32
- // unqualified table name -> aggregate id.
33
- func readProjected(sql, owner string) map[string]string {
34
- out := map[string]string{}
35
- pending := ""
36
-
37
- for _, line := range strings.Split(sql, "\n") {
38
- if found := aggregateComment.FindStringSubmatch(line); found != nil {
39
- pending = aggregateID(found[1], owner)
40
-
41
- continue
42
- }
43
- if found := createTable.FindStringSubmatch(line); found != nil {
44
- if pending != "" {
45
- out[unqualified(found[1])] = pending
46
- }
47
- pending = ""
48
-
49
- continue
50
- }
51
- trimmed := strings.TrimSpace(line)
52
- if trimmed == "" || strings.HasPrefix(trimmed, "--") {
53
- continue
54
- }
55
- // Anything else is a statement, and the note was not about it.
56
- pending = ""
57
- }
58
-
59
- return out
60
- }
61
-
62
- // aggregateID spells the note as an id: a bare slug belongs to the owner of the
63
- // store, a dotted name is already one.
64
- func aggregateID(note, owner string) string {
65
- if strings.Contains(note, ".") {
66
- return note
67
- }
68
-
69
- return owner + "." + slug(note)
70
- }
@@ -1,50 +0,0 @@
1
- package main
2
-
3
- import (
4
- "reflect"
5
- "testing"
6
- )
7
-
8
- // The note names the table it stands above, and only that one. A bare slug is
9
- // an aggregate of the store's owner; a dotted name is somebody's full id.
10
- func TestAggregateNoteBindsToTheNextCreateTable(t *testing.T) {
11
- got := readProjected(`
12
- -- Planned stops, rebuilt from RoutePlanned.
13
- -- aggregate: shop.oms.order
14
- CREATE TABLE route_stops (
15
- route_id text NOT NULL
16
- );
17
-
18
- -- aggregate: price_list
19
- CREATE TABLE IF NOT EXISTS board.prices (
20
- id text PRIMARY KEY
21
- );
22
-
23
- CREATE TABLE seen (
24
- id text PRIMARY KEY
25
- );`, "shop.delivery")
26
-
27
- want := map[string]string{
28
- "route_stops": "shop.oms.order",
29
- "prices": "shop.delivery.price-list",
30
- }
31
- if !reflect.DeepEqual(got, want) {
32
- t.Errorf("projected = %v, want %v", got, want)
33
- }
34
- }
35
-
36
- // A note that a statement gets in front of was not about the table after it,
37
- // and a note nothing follows says nothing.
38
- func TestAggregateNoteDoesNotReachPastAStatement(t *testing.T) {
39
- got := readProjected(`
40
- -- aggregate: shop.oms.order
41
- CREATE INDEX route_stops_by_route ON route_stops (route_id);
42
- CREATE TABLE route_stops_copy (
43
- route_id text NOT NULL
44
- );
45
- -- aggregate: shop.oms.order`, "shop.delivery")
46
-
47
- if len(got) != 0 {
48
- t.Errorf("no table should carry the note, got %v", got)
49
- }
50
- }