@supatype/cli 0.1.5 → 0.1.7

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 (247) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +143 -93
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/deno.d.ts +52 -0
  5. package/dist/augmentation-generator.d.ts +22 -0
  6. package/dist/augmentation-generator.d.ts.map +1 -1
  7. package/dist/augmentation-generator.js +43 -7
  8. package/dist/augmentation-generator.js.map +1 -1
  9. package/dist/binary-cache.d.ts +14 -0
  10. package/dist/binary-cache.d.ts.map +1 -1
  11. package/dist/binary-cache.js +33 -13
  12. package/dist/binary-cache.js.map +1 -1
  13. package/dist/cache-clean.d.ts +15 -0
  14. package/dist/cache-clean.d.ts.map +1 -0
  15. package/dist/cache-clean.js +76 -0
  16. package/dist/cache-clean.js.map +1 -0
  17. package/dist/cache-pins.d.ts +51 -0
  18. package/dist/cache-pins.d.ts.map +1 -0
  19. package/dist/cache-pins.js +184 -0
  20. package/dist/cache-pins.js.map +1 -0
  21. package/dist/cli-install-method.d.ts +19 -0
  22. package/dist/cli-install-method.d.ts.map +1 -0
  23. package/dist/cli-install-method.js +60 -0
  24. package/dist/cli-install-method.js.map +1 -0
  25. package/dist/cli.d.ts.map +1 -1
  26. package/dist/cli.js +6 -0
  27. package/dist/cli.js.map +1 -1
  28. package/dist/cloud-credentials.d.ts +34 -0
  29. package/dist/cloud-credentials.d.ts.map +1 -0
  30. package/dist/cloud-credentials.js +139 -0
  31. package/dist/cloud-credentials.js.map +1 -0
  32. package/dist/commands/admin.d.ts +11 -0
  33. package/dist/commands/admin.d.ts.map +1 -1
  34. package/dist/commands/admin.js +131 -22
  35. package/dist/commands/admin.js.map +1 -1
  36. package/dist/commands/cache.d.ts.map +1 -1
  37. package/dist/commands/cache.js +67 -71
  38. package/dist/commands/cache.js.map +1 -1
  39. package/dist/commands/cloud.d.ts.map +1 -1
  40. package/dist/commands/cloud.js +32 -8
  41. package/dist/commands/cloud.js.map +1 -1
  42. package/dist/commands/db.d.ts +1 -0
  43. package/dist/commands/db.d.ts.map +1 -1
  44. package/dist/commands/db.js +147 -1
  45. package/dist/commands/db.js.map +1 -1
  46. package/dist/commands/dev.d.ts.map +1 -1
  47. package/dist/commands/dev.js +50 -10
  48. package/dist/commands/dev.js.map +1 -1
  49. package/dist/commands/doctor.d.ts +28 -0
  50. package/dist/commands/doctor.d.ts.map +1 -1
  51. package/dist/commands/doctor.js +60 -3
  52. package/dist/commands/doctor.js.map +1 -1
  53. package/dist/commands/functions.d.ts +7 -0
  54. package/dist/commands/functions.d.ts.map +1 -1
  55. package/dist/commands/functions.js +40 -6
  56. package/dist/commands/functions.js.map +1 -1
  57. package/dist/commands/generate.d.ts.map +1 -1
  58. package/dist/commands/generate.js +5 -1
  59. package/dist/commands/generate.js.map +1 -1
  60. package/dist/commands/hooks.d.ts +12 -0
  61. package/dist/commands/hooks.d.ts.map +1 -0
  62. package/dist/commands/hooks.js +118 -0
  63. package/dist/commands/hooks.js.map +1 -0
  64. package/dist/commands/init.d.ts.map +1 -1
  65. package/dist/commands/init.js +41 -7
  66. package/dist/commands/init.js.map +1 -1
  67. package/dist/commands/internal.d.ts +6 -0
  68. package/dist/commands/internal.d.ts.map +1 -0
  69. package/dist/commands/internal.js +22 -0
  70. package/dist/commands/internal.js.map +1 -0
  71. package/dist/commands/keys.d.ts.map +1 -1
  72. package/dist/commands/keys.js +2 -0
  73. package/dist/commands/keys.js.map +1 -1
  74. package/dist/commands/link-helpers.d.ts.map +1 -1
  75. package/dist/commands/link-helpers.js +8 -2
  76. package/dist/commands/link-helpers.js.map +1 -1
  77. package/dist/commands/login.d.ts +7 -0
  78. package/dist/commands/login.d.ts.map +1 -0
  79. package/dist/commands/login.js +110 -0
  80. package/dist/commands/login.js.map +1 -0
  81. package/dist/commands/push.d.ts.map +1 -1
  82. package/dist/commands/push.js +110 -7
  83. package/dist/commands/push.js.map +1 -1
  84. package/dist/commands/self-update.d.ts +7 -0
  85. package/dist/commands/self-update.d.ts.map +1 -1
  86. package/dist/commands/self-update.js +18 -11
  87. package/dist/commands/self-update.js.map +1 -1
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/config.js +18 -3
  90. package/dist/config.js.map +1 -1
  91. package/dist/db-preflight.d.ts +78 -0
  92. package/dist/db-preflight.d.ts.map +1 -0
  93. package/dist/db-preflight.js +452 -0
  94. package/dist/db-preflight.js.map +1 -0
  95. package/dist/dev-compose.d.ts +1 -1
  96. package/dist/dev-compose.d.ts.map +1 -1
  97. package/dist/dev-compose.js +129 -72
  98. package/dist/dev-compose.js.map +1 -1
  99. package/dist/env-file.d.ts +40 -2
  100. package/dist/env-file.d.ts.map +1 -1
  101. package/dist/env-file.js +132 -11
  102. package/dist/env-file.js.map +1 -1
  103. package/dist/field-masking-tier.d.ts +50 -0
  104. package/dist/field-masking-tier.d.ts.map +1 -0
  105. package/dist/field-masking-tier.js +91 -0
  106. package/dist/field-masking-tier.js.map +1 -0
  107. package/dist/functions-deno-types.d.ts +24 -0
  108. package/dist/functions-deno-types.d.ts.map +1 -0
  109. package/dist/functions-deno-types.js +155 -0
  110. package/dist/functions-deno-types.js.map +1 -0
  111. package/dist/hook-upload.d.ts +30 -0
  112. package/dist/hook-upload.d.ts.map +1 -0
  113. package/dist/hook-upload.js +86 -0
  114. package/dist/hook-upload.js.map +1 -0
  115. package/dist/hooks-generator.d.ts +8 -0
  116. package/dist/hooks-generator.d.ts.map +1 -0
  117. package/dist/hooks-generator.js +332 -0
  118. package/dist/hooks-generator.js.map +1 -0
  119. package/dist/link.d.ts +4 -0
  120. package/dist/link.d.ts.map +1 -1
  121. package/dist/link.js +2 -0
  122. package/dist/link.js.map +1 -1
  123. package/dist/local-secrets.d.ts +71 -0
  124. package/dist/local-secrets.d.ts.map +1 -0
  125. package/dist/local-secrets.js +100 -0
  126. package/dist/local-secrets.js.map +1 -0
  127. package/dist/model-hooks.d.ts +73 -0
  128. package/dist/model-hooks.d.ts.map +1 -0
  129. package/dist/model-hooks.js +211 -0
  130. package/dist/model-hooks.js.map +1 -0
  131. package/dist/postgres-ctl.d.ts +11 -0
  132. package/dist/postgres-ctl.d.ts.map +1 -1
  133. package/dist/postgres-ctl.js +25 -2
  134. package/dist/postgres-ctl.js.map +1 -1
  135. package/dist/project-config.d.ts +121 -1
  136. package/dist/project-config.d.ts.map +1 -1
  137. package/dist/project-config.js +150 -1
  138. package/dist/project-config.js.map +1 -1
  139. package/dist/resolve-target.d.ts +4 -0
  140. package/dist/resolve-target.d.ts.map +1 -1
  141. package/dist/resolve-target.js +69 -73
  142. package/dist/resolve-target.js.map +1 -1
  143. package/dist/schema-ast-v2.d.ts +2 -1
  144. package/dist/schema-ast-v2.d.ts.map +1 -1
  145. package/dist/schema-ast-v2.js +4 -2
  146. package/dist/schema-ast-v2.js.map +1 -1
  147. package/dist/schema-push-lock.d.ts.map +1 -1
  148. package/dist/schema-push-lock.js +43 -16
  149. package/dist/schema-push-lock.js.map +1 -1
  150. package/dist/self-host-compose.d.ts +24 -0
  151. package/dist/self-host-compose.d.ts.map +1 -1
  152. package/dist/self-host-compose.js +301 -69
  153. package/dist/self-host-compose.js.map +1 -1
  154. package/dist/service-role-check.d.ts +21 -0
  155. package/dist/service-role-check.d.ts.map +1 -0
  156. package/dist/service-role-check.js +85 -0
  157. package/dist/service-role-check.js.map +1 -0
  158. package/dist/target-client.d.ts +17 -0
  159. package/dist/target-client.d.ts.map +1 -1
  160. package/dist/target-client.js +69 -17
  161. package/dist/target-client.js.map +1 -1
  162. package/dist/type-extractor.d.ts.map +1 -1
  163. package/dist/type-extractor.js +736 -32
  164. package/dist/type-extractor.js.map +1 -1
  165. package/dist/ui/runtime/command-chrome.js +1 -1
  166. package/dist/ui/runtime/command-chrome.js.map +1 -1
  167. package/dist/ui/runtime/stdin-after-ink.d.ts.map +1 -1
  168. package/dist/ui/runtime/stdin-after-ink.js +3 -1
  169. package/dist/ui/runtime/stdin-after-ink.js.map +1 -1
  170. package/package.json +4 -1
  171. package/scripts/generate-homebrew-formula.mjs +121 -0
  172. package/src/augmentation-generator.ts +46 -9
  173. package/src/binary-cache.ts +47 -15
  174. package/src/cache-clean.ts +81 -0
  175. package/src/cache-pins.ts +243 -0
  176. package/src/cli-install-method.ts +67 -0
  177. package/src/cli.ts +6 -0
  178. package/src/cloud-credentials.ts +183 -0
  179. package/src/commands/admin.ts +153 -28
  180. package/src/commands/cache.ts +90 -75
  181. package/src/commands/cloud.ts +32 -8
  182. package/src/commands/db.ts +162 -1
  183. package/src/commands/dev.ts +57 -8
  184. package/src/commands/doctor.ts +69 -3
  185. package/src/commands/functions.ts +42 -5
  186. package/src/commands/generate.ts +5 -1
  187. package/src/commands/hooks.ts +138 -0
  188. package/src/commands/init.ts +42 -8
  189. package/src/commands/internal.ts +23 -0
  190. package/src/commands/keys.ts +2 -0
  191. package/src/commands/link-helpers.ts +9 -2
  192. package/src/commands/login.ts +127 -0
  193. package/src/commands/push.ts +124 -7
  194. package/src/commands/self-update.ts +85 -13
  195. package/src/config.ts +22 -3
  196. package/src/db-preflight.ts +575 -0
  197. package/src/dev-compose.ts +144 -73
  198. package/src/env-file.ts +157 -11
  199. package/src/field-masking-tier.ts +107 -0
  200. package/src/functions-deno-types.ts +188 -0
  201. package/src/hook-upload.ts +107 -0
  202. package/src/hooks-generator.ts +352 -0
  203. package/src/link.ts +7 -0
  204. package/src/local-secrets.ts +104 -0
  205. package/src/model-hooks.ts +260 -0
  206. package/src/postgres-ctl.ts +29 -2
  207. package/src/project-config.ts +236 -2
  208. package/src/resolve-target.ts +113 -68
  209. package/src/schema-ast-v2.ts +5 -2
  210. package/src/schema-push-lock.ts +56 -17
  211. package/src/self-host-compose.ts +344 -68
  212. package/src/service-role-check.ts +111 -0
  213. package/src/target-client.ts +105 -18
  214. package/src/type-extractor.ts +883 -28
  215. package/src/ui/runtime/command-chrome.ts +1 -1
  216. package/src/ui/runtime/stdin-after-ink.ts +3 -1
  217. package/tests/access-composition.test.ts +667 -0
  218. package/tests/admin-studio-membership.test.ts +121 -0
  219. package/tests/api-schemas.test.ts +83 -0
  220. package/tests/archive-name-alias.test.ts +42 -0
  221. package/tests/augmentation-generator.test.ts +72 -4
  222. package/tests/cache-clean.test.ts +206 -0
  223. package/tests/cli-help.test.ts +3 -0
  224. package/tests/cli-install-method.test.ts +66 -0
  225. package/tests/command-chrome-coverage.test.ts +31 -26
  226. package/tests/command-chrome-wrap.test.ts +1 -0
  227. package/tests/db-preflight.test.ts +72 -0
  228. package/tests/doctor-render.test.ts +62 -0
  229. package/tests/doctor-service-role.test.ts +55 -0
  230. package/tests/env-file.test.ts +121 -0
  231. package/tests/external-database-compose.test.ts +184 -0
  232. package/tests/external-database-config.test.ts +154 -0
  233. package/tests/field-masking-tier.test.ts +184 -0
  234. package/tests/functions-deno-types.test.ts +197 -0
  235. package/tests/homebrew-formula.test.ts +58 -0
  236. package/tests/hook-upload.test.ts +125 -0
  237. package/tests/hooks-generator.test.ts +212 -0
  238. package/tests/init.test.ts +3 -0
  239. package/tests/local-secrets.test.ts +92 -0
  240. package/tests/model-hooks.test.ts +322 -0
  241. package/tests/runtime-contract.test.ts +113 -1
  242. package/tests/service-role-check.test.ts +109 -0
  243. package/tests/service-role-routes.test.ts +51 -0
  244. package/tests/target-client-refresh.test.ts +94 -0
  245. package/tests/type-extractor.test.ts +187 -0
  246. package/tsconfig.tsbuildinfo +1 -1
  247. package/tests/engine-distribution.test.ts +0 -418
@@ -0,0 +1,154 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import {
3
+ connectionString,
4
+ externalDatabaseUrl,
5
+ realtimeEnabled,
6
+ usesExternalDatabase,
7
+ validateProjectConfig,
8
+ type SupatypeProjectConfig,
9
+ } from "../src/project-config.js"
10
+
11
+ /**
12
+ * `database.external` points the stack at a Postgres Supatype did not create.
13
+ *
14
+ * Every rejection here is a case where two settings describe the same fact. Picking a silent winner
15
+ * is how a push lands somewhere other than where the services read, which presents as data loss and
16
+ * is not.
17
+ */
18
+ const EXTERNAL_URL = "postgres://owner:secret@db.example.com:5432/app"
19
+
20
+ const raw = (database: unknown, rest: Record<string, unknown> = {}): unknown => ({
21
+ project: { name: "acme" },
22
+ database,
23
+ server: { mode: "dev" },
24
+ app: { mode: "none" },
25
+ ...rest,
26
+ })
27
+
28
+ const validate = (database: unknown, rest?: Record<string, unknown>) =>
29
+ validateProjectConfig(raw(database, rest), "supatype.config.ts")
30
+
31
+ describe("database.external — accepted", () => {
32
+ it("accepts a url on its own, with no database.provider", () => {
33
+ const cfg = validate({ external: { url: EXTERNAL_URL } })
34
+ expect(externalDatabaseUrl(cfg)).toBe(EXTERNAL_URL)
35
+ expect(usesExternalDatabase(cfg)).toBe(true)
36
+ })
37
+
38
+ it("leaves a managed project alone", () => {
39
+ const cfg = validate({ provider: "docker" })
40
+ expect(usesExternalDatabase(cfg)).toBe(false)
41
+ expect(externalDatabaseUrl(cfg)).toBeUndefined()
42
+ })
43
+
44
+ it("accepts postgresql:// as well as postgres://", () => {
45
+ expect(() => validate({ external: { url: "postgresql://u:p@h:5432/d" } })).not.toThrow()
46
+ })
47
+
48
+ it("accepts an identical `connection`, since nothing is ambiguous", () => {
49
+ expect(() =>
50
+ validate({ external: { url: EXTERNAL_URL } }, { connection: EXTERNAL_URL }),
51
+ ).not.toThrow()
52
+ })
53
+ })
54
+
55
+ describe("database.external — rejected", () => {
56
+ it("rejects a missing url", () => {
57
+ expect(() => validate({ external: {} })).toThrow(/database\.external\.url is required/)
58
+ expect(() => validate({ external: { url: " " } })).toThrow(
59
+ /database\.external\.url is required/,
60
+ )
61
+ })
62
+
63
+ it("rejects a url that is not Postgres, naming what it got", () => {
64
+ // A `mysql://` or a bare host is a typo, and the failure it would otherwise cause is six
65
+ // services failing to connect with no mention of the setting responsible.
66
+ expect(() => validate({ external: { url: "mysql://u:p@h/d" } })).toThrow(
67
+ /must be a postgres:\/\/ or postgresql:\/\/ URL \(got "mysql:\/\/u:p@h\/d"\)/,
68
+ )
69
+ expect(() => validate({ external: { url: "db.example.com:5432" } })).toThrow(
70
+ /must be a postgres/,
71
+ )
72
+ })
73
+
74
+ it("rejects external beside database.provider instead of picking one", () => {
75
+ expect(() => validate({ provider: "docker", external: { url: EXTERNAL_URL } })).toThrow(
76
+ /cannot both be set/,
77
+ )
78
+ // And says which to remove, since the top-level `provider` still chooses the runtime stack.
79
+ expect(() => validate({ provider: "native", external: { url: EXTERNAL_URL } })).toThrow(
80
+ /Remove database\.provider/,
81
+ )
82
+ })
83
+
84
+ it("rejects external on the cloud path", () => {
85
+ expect(() =>
86
+ validateProjectConfig(
87
+ {
88
+ project: { name: "acme" },
89
+ database: { external: { url: EXTERNAL_URL } },
90
+ server: { mode: "managed" },
91
+ app: { mode: "none" },
92
+ },
93
+ "supatype.config.ts",
94
+ ),
95
+ ).toThrow(/not supported with server\.mode "managed"/)
96
+ })
97
+
98
+ it("rejects a `connection` that disagrees with the external url", () => {
99
+ expect(() =>
100
+ validate(
101
+ { external: { url: EXTERNAL_URL } },
102
+ { connection: "postgres://elsewhere:5432/other" },
103
+ ),
104
+ ).toThrow(/both set and disagree/)
105
+ })
106
+
107
+ it("rejects a non-boolean realtime flag", () => {
108
+ expect(() => validate({ external: { url: EXTERNAL_URL, realtime: "no" } })).toThrow(
109
+ /realtime must be true or false/,
110
+ )
111
+ })
112
+
113
+ it("rejects a non-object external block", () => {
114
+ expect(() => validate({ external: EXTERNAL_URL })).toThrow(/must be an object with a url/)
115
+ expect(() => validate({ external: [EXTERNAL_URL] })).toThrow(/must be an object with a url/)
116
+ })
117
+ })
118
+
119
+ describe("resolution", () => {
120
+ const external = (realtime?: boolean): SupatypeProjectConfig =>
121
+ validate({
122
+ external: { url: EXTERNAL_URL, ...(realtime !== undefined && { realtime }) },
123
+ })
124
+
125
+ it("uses the external url for CLI commands too", () => {
126
+ // Ahead of DATABASE_URL deliberately: a push that went to the env var's database while the
127
+ // services read from the external one is indistinguishable from losing the data.
128
+ const previous = process.env["DATABASE_URL"]
129
+ process.env["DATABASE_URL"] = "postgres://env:5432/env"
130
+ try {
131
+ expect(connectionString(external())).toBe(EXTERNAL_URL)
132
+ } finally {
133
+ if (previous === undefined) delete process.env["DATABASE_URL"]
134
+ else process.env["DATABASE_URL"] = previous
135
+ }
136
+ })
137
+
138
+ it("leaves DATABASE_URL in charge for a managed project", () => {
139
+ const previous = process.env["DATABASE_URL"]
140
+ process.env["DATABASE_URL"] = "postgres://env:5432/env"
141
+ try {
142
+ expect(connectionString(validate({ provider: "docker" }))).toBe("postgres://env:5432/env")
143
+ } finally {
144
+ if (previous === undefined) delete process.env["DATABASE_URL"]
145
+ else process.env["DATABASE_URL"] = previous
146
+ }
147
+ })
148
+
149
+ it("runs realtime unless told not to", () => {
150
+ expect(realtimeEnabled(external())).toBe(true)
151
+ expect(realtimeEnabled(external(false))).toBe(false)
152
+ expect(realtimeEnabled(validate({ provider: "docker" }))).toBe(true)
153
+ })
154
+ })
@@ -0,0 +1,184 @@
1
+ import { describe, it, expect } from "vitest"
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import { tmpdir } from "node:os"
5
+ import {
6
+ fieldMaskingTier,
7
+ imageShipsMaskExtension,
8
+ nativeMaskLibraryPresent,
9
+ schemaHasFieldRules,
10
+ } from "../src/field-masking-tier.js"
11
+ import { apiSchemaList, validateProjectConfig, type SupatypeProjectConfig } from "../src/project-config.js"
12
+
13
+ /**
14
+ * Which mechanism will enforce per-column rules, decided without touching the database.
15
+ *
16
+ * The engine decides the same thing at push time by asking Postgres whether `supatype_mask` is
17
+ * installed. The CLI has to answer earlier, because it writes `PGRST_DB_SCHEMA` when it generates
18
+ * compose. It can, because the extension needs `shared_preload_libraries` and a compiled library on
19
+ * the host: exactly one path ships it.
20
+ *
21
+ * The rule is deliberately **not** "is the database external". A native `supatype dev` has no
22
+ * extension either, and calling that tier 1 would point PostgREST at a schema whose tables the API
23
+ * roles no longer hold privileges on — every request denied.
24
+ */
25
+ const project = (database: unknown, rest: Record<string, unknown> = {}): SupatypeProjectConfig =>
26
+ validateProjectConfig(
27
+ {
28
+ project: { name: "acme" },
29
+ database,
30
+ server: { mode: "dev" },
31
+ app: { mode: "none" },
32
+ ...rest,
33
+ },
34
+ "supatype.config.ts",
35
+ )
36
+
37
+ const withRules = {
38
+ models: [
39
+ {
40
+ annotations: {
41
+ db: { tableName: "employee" },
42
+ platform: { access: { fields: { salary: { read: { type: "private" } } } } },
43
+ },
44
+ },
45
+ ],
46
+ }
47
+ const withoutRules = {
48
+ models: [{ annotations: { db: { tableName: "employee" }, platform: { access: {} } } }],
49
+ }
50
+
51
+ describe("imageShipsMaskExtension", () => {
52
+ it("is true only for the Supatype Postgres image", () => {
53
+ expect(imageShipsMaskExtension(project({ provider: "docker" }))).toBe(true)
54
+ })
55
+
56
+ it("is false for native, which is the default `supatype dev`", () => {
57
+ // The case that makes "is it external" the wrong rule.
58
+ expect(imageShipsMaskExtension(project({ provider: "native" }))).toBe(false)
59
+ })
60
+
61
+ it("is false for an external database", () => {
62
+ expect(
63
+ imageShipsMaskExtension(project({ external: { url: "postgres://u@h:5432/d" } })),
64
+ ).toBe(false)
65
+ })
66
+
67
+ it("is false when the Postgres image is overridden", () => {
68
+ // Someone running plain `postgres:17` under `provider: docker` has no extension, whatever the
69
+ // provider says.
70
+ expect(imageShipsMaskExtension(project({ provider: "docker", image: "postgres:17" }))).toBe(
71
+ false,
72
+ )
73
+ expect(
74
+ imageShipsMaskExtension(project({ provider: "docker", image: "supatype/postgres:17-latest" })),
75
+ ).toBe(true)
76
+ })
77
+ })
78
+
79
+ describe("schemaHasFieldRules", () => {
80
+ it("sees a masked column", () => {
81
+ expect(schemaHasFieldRules(withRules)).toBe(true)
82
+ })
83
+
84
+ it("is false without one, and for anything unexpected", () => {
85
+ expect(schemaHasFieldRules(withoutRules)).toBe(false)
86
+ expect(schemaHasFieldRules({ models: [] })).toBe(false)
87
+ expect(schemaHasFieldRules(null)).toBe(false)
88
+ expect(schemaHasFieldRules({})).toBe(false)
89
+ })
90
+
91
+ it("is false for an empty fields map", () => {
92
+ // Present but empty means no column is masked, so nothing needs a view.
93
+ const empty = {
94
+ models: [{ annotations: { platform: { access: { fields: {} } } } }],
95
+ }
96
+ expect(schemaHasFieldRules(empty)).toBe(false)
97
+ })
98
+ })
99
+
100
+ describe("fieldMaskingTier", () => {
101
+ it("is none when nothing is masked, whatever the database", () => {
102
+ // With no field rules the engine generates no views, so pointing the API at `api` would serve
103
+ // nothing at all.
104
+ expect(fieldMaskingTier(project({ provider: "native" }), withoutRules)).toBe("none")
105
+ expect(fieldMaskingTier(project({ provider: "docker" }), withoutRules)).toBe("none")
106
+ })
107
+
108
+ it("is extension on the image and views everywhere else", () => {
109
+ expect(fieldMaskingTier(project({ provider: "docker" }), withRules)).toBe("extension")
110
+ expect(fieldMaskingTier(project({ provider: "native" }), withRules)).toBe("views")
111
+ expect(
112
+ fieldMaskingTier(project({ external: { url: "postgres://u@h:5432/d" } }), withRules),
113
+ ).toBe("views")
114
+ })
115
+ })
116
+
117
+ describe("the exposed schema list", () => {
118
+ it("serves from api under tier 2, and drops the managed schema", () => {
119
+ // Both halves matter. A client picks its schema per request with `Accept-Profile`, so leaving the
120
+ // managed schema exposed would let any caller read the unmasked table.
121
+ const list = apiSchemaList(project({ provider: "native" }), "views")
122
+ expect(list).toBe("api, supatype, graphql_public, auth")
123
+ // Entries, not substrings: `graphql_public` legitimately contains "public".
124
+ expect(list.split(", ")).not.toContain("public")
125
+ })
126
+
127
+ it("is unchanged for tier 1 and for a schema with no field rules", () => {
128
+ for (const tier of ["extension", "none", undefined] as const) {
129
+ expect(apiSchemaList(project({ provider: "docker" }), tier)).toBe(
130
+ "public, supatype, graphql_public, auth",
131
+ )
132
+ }
133
+ })
134
+
135
+ it("still lets an explicit api_schemas win", () => {
136
+ // The escape hatch for the one case the offline rule cannot call: a self-managed Postgres where
137
+ // the operator installed the extension themselves.
138
+ const cfg = project({ external: { url: "postgres://u@h:5432/d" } }, {
139
+ schema: { api_schemas: ["public", "supatype"] },
140
+ })
141
+ expect(apiSchemaList(cfg, "views")).toBe("public, supatype")
142
+ })
143
+ })
144
+
145
+ describe("nativeMaskLibraryPresent", () => {
146
+ it("is false for an install without the library", () => {
147
+ // The case that must not be assumed away: an archive downloaded before the library was bundled.
148
+ // Claiming tier 1 for it would select the planner rewrite and the push would be refused.
149
+ const dir = mkdtempSync(join(tmpdir(), "supatype-pg-"))
150
+ mkdirSync(join(dir, "bin"), { recursive: true })
151
+ mkdirSync(join(dir, "lib", "postgresql"), { recursive: true })
152
+ expect(nativeMaskLibraryPresent(join(dir, "bin"))).toBe(false)
153
+ rmSync(dir, { recursive: true, force: true })
154
+ })
155
+
156
+ it("finds the library in either archive layout", () => {
157
+ // Linux and macOS put extension libraries in `lib/`; the Windows archive uses `lib/postgresql/`.
158
+ for (const [subdir, lib] of [
159
+ [["lib"], "supatype_mask.so"],
160
+ [["lib"], "supatype_mask.dylib"],
161
+ [["lib", "postgresql"], "supatype_mask.dll"],
162
+ ] as const) {
163
+ const dir = mkdtempSync(join(tmpdir(), "supatype-pg-"))
164
+ mkdirSync(join(dir, "bin"), { recursive: true })
165
+ mkdirSync(join(dir, ...subdir), { recursive: true })
166
+ writeFileSync(join(dir, ...subdir, lib), "")
167
+ expect(nativeMaskLibraryPresent(join(dir, "bin")), lib).toBe(true)
168
+ rmSync(dir, { recursive: true, force: true })
169
+ }
170
+ })
171
+
172
+ it("is false without an install directory at all", () => {
173
+ expect(nativeMaskLibraryPresent(null)).toBe(false)
174
+ expect(nativeMaskLibraryPresent(undefined)).toBe(false)
175
+ })
176
+
177
+ it("selects the extension tier when the library is installed", () => {
178
+ // The point of the whole change: native dev enforces field rules the same way the image does.
179
+ expect(fieldMaskingTier(project({ provider: "native" }), withRules, true)).toBe("extension")
180
+ expect(fieldMaskingTier(project({ provider: "native" }), withRules, false)).toBe("views")
181
+ // Still nothing to enforce when no column is masked.
182
+ expect(fieldMaskingTier(project({ provider: "native" }), withoutRules, true)).toBe("none")
183
+ })
184
+ })
@@ -0,0 +1,197 @@
1
+ import { spawnSync } from "node:child_process"
2
+ import { createRequire } from "node:module"
3
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs"
4
+ import { tmpdir } from "node:os"
5
+ import { dirname, join } from "node:path"
6
+ import { afterEach, describe, expect, it } from "vitest"
7
+ import {
8
+ ensureFunctionsDenoTypes,
9
+ functionsDenoAmbientSource,
10
+ functionsTsConfigSource,
11
+ } from "../src/functions-deno-types.js"
12
+
13
+ const require = createRequire(import.meta.url)
14
+
15
+ function resolveTscBin(): string {
16
+ const typescriptEntry = require.resolve("typescript/package.json")
17
+ return join(dirname(typescriptEntry), "bin", "tsc")
18
+ }
19
+
20
+ function runTsc(projectDir: string): { status: number | null; stdout: string; stderr: string } {
21
+ const result = spawnSync(process.execPath, [resolveTscBin(), "-p", "tsconfig.json", "--pretty", "false"], {
22
+ cwd: projectDir,
23
+ encoding: "utf8",
24
+ })
25
+ return {
26
+ status: result.status,
27
+ stdout: result.stdout ?? "",
28
+ stderr: result.stderr ?? "",
29
+ }
30
+ }
31
+
32
+ const HANDLER_WITH_DENO = `export default async function handler(req: Request): Promise<Response> {
33
+ const url = Deno.env.get("SUPATYPE_URL")
34
+ return new Response(JSON.stringify({ url, method: req.method }), {
35
+ headers: { "Content-Type": "application/json" },
36
+ })
37
+ }
38
+ `
39
+
40
+ describe("ensureFunctionsDenoTypes", () => {
41
+ let tmpRoot = ""
42
+
43
+ afterEach(() => {
44
+ if (tmpRoot) rmSync(tmpRoot, { recursive: true, force: true })
45
+ tmpRoot = ""
46
+ })
47
+
48
+ it("writes deno.d.ts and tsconfig.json under functions/", () => {
49
+ tmpRoot = mkdtempSync(join(tmpdir(), "supatype-deno-types-"))
50
+ const functionsDir = join(tmpRoot, "functions")
51
+
52
+ const result = ensureFunctionsDenoTypes(tmpRoot, functionsDir)
53
+
54
+ expect(result.wroteDenoDts).toBe(true)
55
+ expect(result.wroteTsconfig).toBe(true)
56
+ expect(existsSync(join(functionsDir, "deno.d.ts"))).toBe(true)
57
+ expect(existsSync(join(functionsDir, "tsconfig.json"))).toBe(true)
58
+ expect(readFileSync(join(functionsDir, "deno.d.ts"), "utf8")).toContain("declare namespace Deno")
59
+ expect(readFileSync(join(functionsDir, "tsconfig.json"), "utf8")).toContain('"types": []')
60
+ })
61
+
62
+ it("is idempotent when files already exist", () => {
63
+ tmpRoot = mkdtempSync(join(tmpdir(), "supatype-deno-types-"))
64
+ const functionsDir = join(tmpRoot, "functions")
65
+ mkdirSync(functionsDir, { recursive: true })
66
+ writeFileSync(join(functionsDir, "deno.d.ts"), "// custom\n", "utf8")
67
+ writeFileSync(join(functionsDir, "tsconfig.json"), "{}\n", "utf8")
68
+
69
+ const result = ensureFunctionsDenoTypes(tmpRoot, functionsDir)
70
+
71
+ expect(result.wroteDenoDts).toBe(false)
72
+ expect(result.wroteTsconfig).toBe(false)
73
+ expect(readFileSync(join(functionsDir, "deno.d.ts"), "utf8")).toBe("// custom\n")
74
+ })
75
+
76
+ it("adds functions to root tsconfig exclude when possible", () => {
77
+ tmpRoot = mkdtempSync(join(tmpdir(), "supatype-deno-types-"))
78
+ writeFileSync(
79
+ join(tmpRoot, "tsconfig.json"),
80
+ JSON.stringify({
81
+ compilerOptions: { strict: true },
82
+ include: ["**/*.ts"],
83
+ exclude: ["node_modules"],
84
+ }, null, 2),
85
+ "utf8",
86
+ )
87
+ const functionsDir = join(tmpRoot, "functions")
88
+
89
+ const result = ensureFunctionsDenoTypes(tmpRoot, functionsDir)
90
+
91
+ expect(result.rootExclude).toBe("updated")
92
+ const root = JSON.parse(readFileSync(join(tmpRoot, "tsconfig.json"), "utf8")) as {
93
+ exclude: string[]
94
+ }
95
+ expect(root.exclude).toContain("functions")
96
+ expect(root.exclude).toContain("node_modules")
97
+ })
98
+
99
+ it("reports already when functions is already excluded", () => {
100
+ tmpRoot = mkdtempSync(join(tmpdir(), "supatype-deno-types-"))
101
+ writeFileSync(
102
+ join(tmpRoot, "tsconfig.json"),
103
+ JSON.stringify({ exclude: ["node_modules", "functions"] }, null, 2),
104
+ "utf8",
105
+ )
106
+
107
+ const result = ensureFunctionsDenoTypes(tmpRoot, join(tmpRoot, "functions"))
108
+ expect(result.rootExclude).toBe("already")
109
+ })
110
+
111
+ it("templates include Deno.env", () => {
112
+ expect(functionsDenoAmbientSource()).toContain("const env: DenoEnv")
113
+ expect(functionsTsConfigSource()).toContain("deno.d.ts")
114
+ })
115
+ })
116
+
117
+ /**
118
+ * Manual / end-to-end check: real `tsc` against a scaffolded functions dir.
119
+ * Proves Deno.env is typed (no TS2304) once deno.d.ts + functions/tsconfig exist.
120
+ */
121
+ describe("functions Deno types (tsc manual check)", () => {
122
+ let tmpRoot = ""
123
+
124
+ afterEach(() => {
125
+ if (tmpRoot) rmSync(tmpRoot, { recursive: true, force: true })
126
+ tmpRoot = ""
127
+ })
128
+
129
+ it("tsc fails with Cannot find name Deno without ambient types", () => {
130
+ tmpRoot = mkdtempSync(join(tmpdir(), "supatype-deno-tsc-baseline-"))
131
+ const fnDir = join(tmpRoot, "hello")
132
+ mkdirSync(fnDir, { recursive: true })
133
+ writeFileSync(join(fnDir, "index.ts"), HANDLER_WITH_DENO, "utf8")
134
+ writeFileSync(
135
+ join(tmpRoot, "tsconfig.json"),
136
+ JSON.stringify(
137
+ {
138
+ compilerOptions: {
139
+ target: "ES2022",
140
+ lib: ["ES2022", "DOM"],
141
+ strict: true,
142
+ noEmit: true,
143
+ module: "ESNext",
144
+ moduleResolution: "bundler",
145
+ types: [],
146
+ },
147
+ include: ["./**/*.ts"],
148
+ },
149
+ null,
150
+ 2,
151
+ ),
152
+ "utf8",
153
+ )
154
+
155
+ const { status, stdout, stderr } = runTsc(tmpRoot)
156
+ const out = `${stdout}\n${stderr}`
157
+ expect(status).not.toBe(0)
158
+ expect(out).toMatch(/Cannot find name 'Deno'|TS2304/)
159
+ })
160
+
161
+ it("tsc passes for Deno.env after ensureFunctionsDenoTypes", () => {
162
+ tmpRoot = mkdtempSync(join(tmpdir(), "supatype-deno-tsc-ok-"))
163
+ const functionsDir = join(tmpRoot, "functions")
164
+ mkdirSync(join(functionsDir, "hello"), { recursive: true })
165
+ writeFileSync(join(functionsDir, "hello", "index.ts"), HANDLER_WITH_DENO, "utf8")
166
+ writeFileSync(
167
+ join(tmpRoot, "tsconfig.json"),
168
+ JSON.stringify(
169
+ {
170
+ compilerOptions: {
171
+ target: "ES2022",
172
+ lib: ["ES2022", "DOM"],
173
+ strict: true,
174
+ noEmit: true,
175
+ module: "ESNext",
176
+ moduleResolution: "bundler",
177
+ },
178
+ include: ["**/*.ts"],
179
+ exclude: ["node_modules"],
180
+ },
181
+ null,
182
+ 2,
183
+ ),
184
+ "utf8",
185
+ )
186
+
187
+ const ensured = ensureFunctionsDenoTypes(tmpRoot, functionsDir)
188
+ expect(ensured.wroteDenoDts).toBe(true)
189
+ expect(ensured.wroteTsconfig).toBe(true)
190
+ expect(ensured.rootExclude).toBe("updated")
191
+
192
+ const { status, stdout, stderr } = runTsc(functionsDir)
193
+ const out = `${stdout}\n${stderr}`
194
+ expect(out, `tsc stderr/stdout:\n${out}`).not.toMatch(/Cannot find name 'Deno'|TS2304/)
195
+ expect(status, `tsc failed:\n${out}`).toBe(0)
196
+ })
197
+ })
@@ -0,0 +1,58 @@
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { tmpdir } from "node:os"
4
+ import { spawnSync } from "node:child_process"
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest"
6
+
7
+ let counter = 0
8
+ let tmpDir: string
9
+
10
+ const PLATFORMS = [
11
+ "supatype-cli-darwin-arm64",
12
+ "supatype-cli-darwin-amd64",
13
+ "supatype-cli-linux-arm64",
14
+ "supatype-cli-linux-amd64",
15
+ ]
16
+
17
+ beforeEach(() => {
18
+ tmpDir = join(tmpdir(), `homebrew-formula-${Date.now()}-${++counter}`)
19
+ mkdirSync(tmpDir, { recursive: true })
20
+ for (const name of PLATFORMS) {
21
+ writeFileSync(join(tmpDir, name), `stub-${name}\n`, "utf8")
22
+ }
23
+ })
24
+
25
+ afterEach(() => {
26
+ rmSync(tmpDir, { recursive: true, force: true })
27
+ })
28
+
29
+ describe("generate-homebrew-formula.mjs", () => {
30
+ it("writes a Formula with versioned CDN URLs and sha256 blocks", () => {
31
+ const outPath = join(tmpDir, "supatype.rb")
32
+ const script = join(import.meta.dirname, "../scripts/generate-homebrew-formula.mjs")
33
+ const result = spawnSync(
34
+ process.execPath,
35
+ [script, "9.9.9", tmpDir],
36
+ {
37
+ encoding: "utf8",
38
+ env: { ...process.env, HOMEBREW_FORMULA_OUT: outPath },
39
+ },
40
+ )
41
+
42
+ expect(result.status, result.stderr).toBe(0)
43
+ const formula = readFileSync(outPath, "utf8")
44
+ expect(formula).toContain('version "9.9.9"')
45
+ expect(formula).toContain("https://releases.supatype.com/cli/v9.9.9/supatype-cli-darwin-arm64")
46
+ expect(formula).toMatch(/sha256 "[a-f0-9]{64}"/)
47
+ expect(formula).toContain('system bin/"supatype", "_postinstall"')
48
+ expect(formula).toContain('bin.install "supatype-cli-#{arch}" => "supatype"')
49
+ })
50
+
51
+ it("fails when an artifact is missing", () => {
52
+ rmSync(join(tmpDir, "supatype-cli-linux-amd64"))
53
+ const script = join(import.meta.dirname, "../scripts/generate-homebrew-formula.mjs")
54
+ const result = spawnSync(process.execPath, [script, "1.0.0", tmpDir], { encoding: "utf8" })
55
+ expect(result.status).not.toBe(0)
56
+ expect(result.stderr).toContain("Missing artifact")
57
+ })
58
+ })