@supatype/cli 0.1.12 → 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 (143) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +150 -136
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/cli-version-embedded.d.ts.map +1 -1
  5. package/dist/cli-version-embedded.js +1 -1
  6. package/dist/cli-version-embedded.js.map +1 -1
  7. package/dist/commands/admin.d.ts +5 -5
  8. package/dist/commands/admin.d.ts.map +1 -1
  9. package/dist/commands/admin.js +10 -10
  10. package/dist/commands/admin.js.map +1 -1
  11. package/dist/commands/db.d.ts.map +1 -1
  12. package/dist/commands/db.js +23 -1
  13. package/dist/commands/db.js.map +1 -1
  14. package/dist/commands/dev.js +51 -51
  15. package/dist/commands/dev.js.map +1 -1
  16. package/dist/commands/doctor.d.ts +0 -7
  17. package/dist/commands/doctor.d.ts.map +1 -1
  18. package/dist/commands/doctor.js +26 -0
  19. package/dist/commands/doctor.js.map +1 -1
  20. package/dist/commands/init.js +1 -1
  21. package/dist/commands/logs.d.ts +14 -1
  22. package/dist/commands/logs.d.ts.map +1 -1
  23. package/dist/commands/logs.js +33 -37
  24. package/dist/commands/logs.js.map +1 -1
  25. package/dist/commands/push.d.ts.map +1 -1
  26. package/dist/commands/push.js +18 -7
  27. package/dist/commands/push.js.map +1 -1
  28. package/dist/commands/status.d.ts +1 -1
  29. package/dist/commands/status.d.ts.map +1 -1
  30. package/dist/commands/status.js +33 -64
  31. package/dist/commands/status.js.map +1 -1
  32. package/dist/compose-local-server-image.d.ts +11 -0
  33. package/dist/compose-local-server-image.d.ts.map +1 -1
  34. package/dist/compose-local-server-image.js +18 -0
  35. package/dist/compose-local-server-image.js.map +1 -1
  36. package/dist/compose-services.d.ts +59 -0
  37. package/dist/compose-services.d.ts.map +1 -0
  38. package/dist/compose-services.js +160 -0
  39. package/dist/compose-services.js.map +1 -0
  40. package/dist/config.d.ts +1 -1
  41. package/dist/config.d.ts.map +1 -1
  42. package/dist/dev-compose.d.ts +1 -0
  43. package/dist/dev-compose.d.ts.map +1 -1
  44. package/dist/dev-compose.js +202 -30
  45. package/dist/dev-compose.js.map +1 -1
  46. package/dist/dev-ports.d.ts +24 -0
  47. package/dist/dev-ports.d.ts.map +1 -1
  48. package/dist/dev-ports.js +37 -0
  49. package/dist/dev-ports.js.map +1 -1
  50. package/dist/docker-postgres.d.ts +1 -1
  51. package/dist/docker-postgres.d.ts.map +1 -1
  52. package/dist/docker-postgres.js +2 -2
  53. package/dist/docker-postgres.js.map +1 -1
  54. package/dist/engine-floor.d.ts +50 -0
  55. package/dist/engine-floor.d.ts.map +1 -0
  56. package/dist/engine-floor.js +80 -0
  57. package/dist/engine-floor.js.map +1 -0
  58. package/dist/field-bounds.d.ts +68 -0
  59. package/dist/field-bounds.d.ts.map +1 -0
  60. package/dist/field-bounds.js +277 -0
  61. package/dist/field-bounds.js.map +1 -0
  62. package/dist/hooks-generator.d.ts +1 -1
  63. package/dist/hooks-generator.d.ts.map +1 -1
  64. package/dist/hooks-generator.js +78 -4
  65. package/dist/hooks-generator.js.map +1 -1
  66. package/dist/link.d.ts +1 -1
  67. package/dist/link.d.ts.map +1 -1
  68. package/dist/model-hooks.d.ts +44 -2
  69. package/dist/model-hooks.d.ts.map +1 -1
  70. package/dist/model-hooks.js +116 -12
  71. package/dist/model-hooks.js.map +1 -1
  72. package/dist/pgbouncer-userlist.d.ts +1 -1
  73. package/dist/pgbouncer-userlist.d.ts.map +1 -1
  74. package/dist/pgbouncer-userlist.js +1 -1
  75. package/dist/pgbouncer-userlist.js.map +1 -1
  76. package/dist/postgres-ctl.d.ts +5 -1
  77. package/dist/postgres-ctl.d.ts.map +1 -1
  78. package/dist/postgres-ctl.js +43 -4
  79. package/dist/postgres-ctl.js.map +1 -1
  80. package/dist/project-config.d.ts +4 -4
  81. package/dist/resolve-target.d.ts +1 -1
  82. package/dist/resolve-target.d.ts.map +1 -1
  83. package/dist/schema-ast-v2.d.ts +38 -4
  84. package/dist/schema-ast-v2.d.ts.map +1 -1
  85. package/dist/schema-ast-v2.js +87 -4
  86. package/dist/schema-ast-v2.js.map +1 -1
  87. package/dist/self-host-compose.d.ts.map +1 -1
  88. package/dist/self-host-compose.js +41 -18
  89. package/dist/self-host-compose.js.map +1 -1
  90. package/dist/studio-dev-server.d.ts +8 -1
  91. package/dist/studio-dev-server.d.ts.map +1 -1
  92. package/dist/studio-dev-server.js +1 -2
  93. package/dist/studio-dev-server.js.map +1 -1
  94. package/dist/type-extractor.d.ts.map +1 -1
  95. package/dist/type-extractor.js +309 -27
  96. package/dist/type-extractor.js.map +1 -1
  97. package/package.json +4 -3
  98. package/src/cli-version-embedded.ts +1 -1
  99. package/src/commands/admin.ts +10 -10
  100. package/src/commands/db.ts +27 -1
  101. package/src/commands/dev.ts +51 -51
  102. package/src/commands/doctor.ts +30 -0
  103. package/src/commands/init.ts +1 -1
  104. package/src/commands/logs.ts +34 -37
  105. package/src/commands/push.ts +29 -6
  106. package/src/commands/status.ts +34 -70
  107. package/src/compose-local-server-image.ts +17 -0
  108. package/src/compose-services.ts +174 -0
  109. package/src/config.ts +1 -1
  110. package/src/dev-compose.ts +226 -29
  111. package/src/dev-ports.ts +48 -0
  112. package/src/docker-postgres.ts +2 -2
  113. package/src/engine-floor.ts +113 -0
  114. package/src/field-bounds.ts +359 -0
  115. package/src/hooks-generator.ts +81 -4
  116. package/src/link.ts +1 -1
  117. package/src/model-hooks.ts +158 -12
  118. package/src/pgbouncer-userlist.ts +1 -1
  119. package/src/postgres-ctl.ts +44 -4
  120. package/src/project-config.ts +4 -4
  121. package/src/resolve-target.ts +1 -1
  122. package/src/schema-ast-v2.ts +114 -10
  123. package/src/self-host-compose.ts +41 -18
  124. package/src/studio-dev-server.ts +9 -3
  125. package/src/type-extractor.ts +374 -39
  126. package/tests/admin-ensure.test.ts +10 -10
  127. package/tests/compose-parsable-defaults.test.ts +60 -0
  128. package/tests/compose-services.test.ts +144 -0
  129. package/tests/dev-ports-owner.test.ts +40 -0
  130. package/tests/engine-floor.test.ts +102 -0
  131. package/tests/external-database-compose.test.ts +3 -3
  132. package/tests/field-bounds-matrix.test.ts +163 -0
  133. package/tests/field-bounds.test.ts +139 -0
  134. package/tests/field-validators.test.ts +139 -0
  135. package/tests/hooks-generator.test.ts +86 -0
  136. package/tests/local-server-image-env.test.ts +93 -0
  137. package/tests/model-constraints.test.ts +293 -0
  138. package/tests/model-hooks.test.ts +56 -0
  139. package/tests/port-in-use.test.ts +58 -0
  140. package/tests/readiness-poll.test.ts +84 -0
  141. package/tests/runtime-contract.test.ts +1 -1
  142. package/tests/type-extractor.test.ts +49 -0
  143. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,58 @@
1
+ import { createServer, type Server } from "node:net"
2
+ import { afterEach, describe, expect, it } from "vitest"
3
+ import { isPortInUse } from "../src/postgres-ctl.js"
4
+
5
+ /**
6
+ * `supatype dev` asks this before handing a port to Docker Compose, so a wrong answer costs a
7
+ * failed `compose up`, not a warning:
8
+ *
9
+ * Bind for 0.0.0.0:5432 failed: port is already allocated
10
+ *
11
+ * The Docker case cannot be reproduced here, because it needs Docker Desktop's port proxy, which
12
+ * publishes without holding a bind that a socket can see. It is why this function connects as well
13
+ * as binds; the loopback case below is what a unit test can hold onto.
14
+ */
15
+
16
+ // Ports in the high range this repository does not otherwise use.
17
+ const PORT = 19871
18
+
19
+ let server: Server | null = null
20
+
21
+ function listen(port: number, host: string): Promise<Server> {
22
+ return new Promise((resolve, reject) => {
23
+ const created = createServer()
24
+ created.once("error", reject)
25
+ created.listen(port, host, () => resolve(created))
26
+ })
27
+ }
28
+
29
+ afterEach(async () => {
30
+ if (server !== null) {
31
+ await new Promise<void>((resolve) => server!.close(() => resolve()))
32
+ server = null
33
+ }
34
+ })
35
+
36
+ describe("isPortInUse", () => {
37
+ it("reports a free port as free", async () => {
38
+ await expect(isPortInUse(PORT)).resolves.toBe(false)
39
+ })
40
+
41
+ it("reports a port with a loopback listener as in use", async () => {
42
+ server = await listen(PORT, "127.0.0.1")
43
+ await expect(isPortInUse(PORT)).resolves.toBe(true)
44
+ })
45
+
46
+ it("reports a port with a wildcard listener as in use", async () => {
47
+ server = await listen(PORT, "0.0.0.0")
48
+ await expect(isPortInUse(PORT)).resolves.toBe(true)
49
+ })
50
+
51
+ it("reports the port free again once the listener closes", async () => {
52
+ server = await listen(PORT, "127.0.0.1")
53
+ await expect(isPortInUse(PORT)).resolves.toBe(true)
54
+ await new Promise<void>((resolve) => server!.close(() => resolve()))
55
+ server = null
56
+ await expect(isPortInUse(PORT)).resolves.toBe(false)
57
+ })
58
+ })
@@ -0,0 +1,84 @@
1
+ import { createServer, type Server } from "node:http"
2
+ import { readFileSync } from "node:fs"
3
+ import { fileURLToPath } from "node:url"
4
+ import type { AddressInfo } from "node:net"
5
+ import { afterEach, describe, expect, it } from "vitest"
6
+
7
+ /**
8
+ * A readiness poll has to be able to give up, and has to poll an address that is bound.
9
+ *
10
+ * `supatype push` reported the schema applied and then appeared to hang, on a wait whose own error
11
+ * message advertised 120 seconds. Two things were wrong. The loop was bounded by iterations rather
12
+ * than by time, and each `fetch` carried no deadline of its own, so against a socket that accepts
13
+ * and never answers undici's ~30s default applied instead of the intended 1s: "120 attempts, one
14
+ * second apart" is really closer to an hour. And it polled `localhost`, which resolves to `::1`
15
+ * first, where Docker Desktop's IPv6 relay accepts and then goes silent when unhealthy. Docker
16
+ * publishes on `0.0.0.0`, so IPv4 is the address certainly bound.
17
+ */
18
+
19
+ const servers: Server[] = []
20
+
21
+ afterEach(async () => {
22
+ await Promise.all(
23
+ servers.splice(0).map((s) => new Promise<void>((resolve) => s.close(() => resolve()))),
24
+ )
25
+ })
26
+
27
+ /** A server that accepts connections and never responds, like the unhealthy relay. */
28
+ async function blackHole(): Promise<number> {
29
+ const server = createServer(() => {
30
+ /* deliberately no response, and no socket close */
31
+ })
32
+ servers.push(server)
33
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve))
34
+ return (server.address() as AddressInfo).port
35
+ }
36
+
37
+ const devCompose = (): string =>
38
+ readFileSync(fileURLToPath(new URL("../src/dev-compose.ts", import.meta.url)), "utf8")
39
+
40
+ describe("a readiness attempt against a port that accepts and never answers", () => {
41
+ it("ends promptly when it carries a deadline", async () => {
42
+ const port = await blackHole()
43
+ const started = Date.now()
44
+
45
+ await expect(
46
+ fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(300) }),
47
+ ).rejects.toThrow()
48
+
49
+ // Generous, because the point is that it returns at all rather than how fast.
50
+ expect(Date.now() - started).toBeLessThan(3000)
51
+ })
52
+ })
53
+
54
+ describe("the polls the CLI actually runs", () => {
55
+ it("give every attempt a deadline", () => {
56
+ const source = devCompose()
57
+ // `fetchWithin` is the only way to reach the gateway from a poll loop. A bare `fetch` inside
58
+ // one is the bug: the loop's iteration count stops being its real bound.
59
+ const helper = source.match(/async function fetchWithin\(/g) ?? []
60
+ expect(helper.length, "the timeout helper should exist").toBe(1)
61
+
62
+ const kongPoll = source.slice(
63
+ source.indexOf("async function waitKongReady("),
64
+ source.indexOf("async function provisionDockerStorageBuckets("),
65
+ )
66
+ // Strip the wrapper's own name before looking for a bare call, or `fetchWithin(` matches
67
+ // `fetch(` and the check passes on exactly the code it exists to reject.
68
+ const bare = kongPoll.replaceAll("fetchWithin(", "<<ok>>")
69
+ expect(bare, "a poll loop must not call fetch without a deadline").not.toMatch(/\bfetch\(/)
70
+ expect(kongPoll).toMatch(/fetchWithin\(/)
71
+ })
72
+
73
+ it("target IPv4, not a name that may resolve to an unbound address", () => {
74
+ const source = devCompose()
75
+ const kongPoll = source.slice(
76
+ source.indexOf("async function waitKongReady("),
77
+ source.indexOf("async function provisionDockerStorageBuckets("),
78
+ )
79
+ // The URLs written to `.env` and printed for the operator stay on `localhost`, deliberately:
80
+ // those are for a browser. This is about what the CLI dials itself.
81
+ expect(kongPoll).not.toContain("http://localhost:")
82
+ expect(kongPoll).toMatch(/loopbackBase\(/)
83
+ })
84
+ })
@@ -163,7 +163,7 @@ describe("runtime contract", () => {
163
163
  })
164
164
 
165
165
  // The whole point of requiring them: a service must never fall back to a secret published in
166
- // this repository. That default used to reach PostgREST, storage and GoTrue.
166
+ // this repository. That default used to reach PostgREST, storage and auth.
167
167
  it("never defaults a secret to a published constant", () => {
168
168
  const compose = renderSelfHostCompose(baseConfig)
169
169
  expect(compose).not.toContain("super-secret-jwt-token")
@@ -1322,4 +1322,53 @@ export type Media = Bucket<"media", {
1322
1322
  /Bucket "media": could not resolve its `access` rules/,
1323
1323
  )
1324
1324
  })
1325
+
1326
+ it("emits structured validation bounds alongside the compiled check constraint", () => {
1327
+ const dir = mkdtempSync(join(tmpdir(), "supatype-validation-"))
1328
+ dirs.push(dir)
1329
+ const schemaPath = join(dir, "schema.ts")
1330
+ writeFileSync(
1331
+ schemaPath,
1332
+ `
1333
+ import type {
1334
+ Model, Public, UUID, Int, Optional, MaxLength, MinLength, Between,
1335
+ } from "@supatype/types"
1336
+
1337
+ export type Review = Model<{
1338
+ id: UUID
1339
+ headline: MaxLength<string, 120>
1340
+ body: MinLength<MaxLength<string, 4000>, 20>
1341
+ rating: Between<Int, 1, 5>
1342
+ note: Optional<string>
1343
+ }, {
1344
+ access: { read: Public }
1345
+ }>
1346
+ `,
1347
+ "utf8",
1348
+ )
1349
+
1350
+ const ast = extractSchemaAstFromTypes(schemaPath, dir)
1351
+ const review = ast?.models.find((m) => m.name === "Review")
1352
+ expect(review).toBeDefined()
1353
+
1354
+ expect(review?.fields["headline"]).toMatchObject({
1355
+ check: 'char_length("{name}") <= 120',
1356
+ validation: { maxLength: 120 },
1357
+ })
1358
+
1359
+ // Stacked modifiers merge into one check and one validation object.
1360
+ expect(review?.fields["body"]).toMatchObject({
1361
+ validation: { maxLength: 4000, minLength: 20 },
1362
+ })
1363
+ expect(review?.fields["body"]?.["check"]).toContain('char_length("{name}") <= 4000')
1364
+ expect(review?.fields["body"]?.["check"]).toContain('char_length("{name}") >= 20')
1365
+
1366
+ expect(review?.fields["rating"]).toMatchObject({
1367
+ kind: "integer",
1368
+ validation: { min: 1, max: 5 },
1369
+ })
1370
+
1371
+ // A field with no constraint modifier carries no validation key at all.
1372
+ expect(review?.fields["note"]?.["validation"]).toBeUndefined()
1373
+ })
1325
1374
  })