@supatype/cli 0.1.0-alpha.7 → 0.1.0-alpha.8
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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +67 -62
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/app/proxy-dev-app.d.ts +13 -0
- package/dist/app/proxy-dev-app.d.ts.map +1 -0
- package/dist/app/proxy-dev-app.js +53 -0
- package/dist/app/proxy-dev-app.js.map +1 -0
- package/dist/binary-cache.d.ts +5 -0
- package/dist/binary-cache.d.ts.map +1 -1
- package/dist/binary-cache.js +13 -0
- package/dist/binary-cache.js.map +1 -1
- package/dist/commands/cloud.d.ts +11 -3
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +33 -25
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +3 -17
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +3 -3
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +66 -59
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +11 -1
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/init.js +16 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +42 -12
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +16 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/dev-compose.d.ts +17 -0
- package/dist/dev-compose.d.ts.map +1 -0
- package/dist/dev-compose.js +374 -0
- package/dist/dev-compose.js.map +1 -0
- package/dist/diff-output.d.ts +4 -0
- package/dist/diff-output.d.ts.map +1 -0
- package/dist/diff-output.js +12 -0
- package/dist/diff-output.js.map +1 -0
- package/dist/docker-postgres.d.ts +21 -3
- package/dist/docker-postgres.d.ts.map +1 -1
- package/dist/docker-postgres.js +130 -18
- package/dist/docker-postgres.js.map +1 -1
- package/dist/engine-client.d.ts +5 -3
- package/dist/engine-client.d.ts.map +1 -1
- package/dist/engine-client.js +2 -1
- package/dist/engine-client.js.map +1 -1
- package/dist/kong-config.d.ts +4 -0
- package/dist/kong-config.d.ts.map +1 -1
- package/dist/kong-config.js +12 -1
- package/dist/kong-config.js.map +1 -1
- package/dist/process-manager.d.ts +2 -0
- package/dist/process-manager.d.ts.map +1 -1
- package/dist/process-manager.js +16 -1
- package/dist/process-manager.js.map +1 -1
- package/dist/project-config.d.ts +21 -1
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +15 -0
- package/dist/project-config.js.map +1 -1
- package/dist/runtime-routes.d.ts +9 -0
- package/dist/runtime-routes.d.ts.map +1 -1
- package/dist/runtime-routes.js +75 -12
- package/dist/runtime-routes.js.map +1 -1
- package/dist/schema-ast-v2.d.ts +127 -0
- package/dist/schema-ast-v2.d.ts.map +1 -0
- package/dist/schema-ast-v2.js +226 -0
- package/dist/schema-ast-v2.js.map +1 -0
- package/dist/self-host-compose.d.ts +12 -4
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +146 -35
- package/dist/self-host-compose.js.map +1 -1
- package/dist/studio-admin-roles.d.ts +7 -0
- package/dist/studio-admin-roles.d.ts.map +1 -0
- package/dist/studio-admin-roles.js +14 -0
- package/dist/studio-admin-roles.js.map +1 -0
- package/dist/studio-dev-server.d.ts +22 -0
- package/dist/studio-dev-server.d.ts.map +1 -0
- package/dist/studio-dev-server.js +28 -0
- package/dist/studio-dev-server.js.map +1 -0
- package/dist/type-extractor.d.ts +3 -30
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +485 -148
- package/dist/type-extractor.js.map +1 -1
- package/dist/type-resolver.d.ts +33 -0
- package/dist/type-resolver.d.ts.map +1 -0
- package/dist/type-resolver.js +338 -0
- package/dist/type-resolver.js.map +1 -0
- package/package.json +1 -1
- package/src/TYPE-RESOLUTION.md +294 -0
- package/src/app/proxy-dev-app.ts +67 -0
- package/src/binary-cache.ts +20 -0
- package/src/commands/cloud.ts +40 -30
- package/src/commands/deploy.ts +3 -18
- package/src/commands/dev.ts +72 -69
- package/src/commands/diff.ts +11 -1
- package/src/commands/init.ts +16 -3
- package/src/commands/push.ts +49 -13
- package/src/commands/update.ts +17 -0
- package/src/dev-compose.ts +455 -0
- package/src/diff-output.ts +12 -0
- package/src/docker-postgres.ts +184 -27
- package/src/engine-client.ts +9 -4
- package/src/kong-config.ts +16 -1
- package/src/process-manager.ts +18 -1
- package/src/project-config.ts +34 -1
- package/src/runtime-routes.ts +87 -12
- package/src/schema-ast-v2.ts +324 -0
- package/src/self-host-compose.ts +168 -36
- package/src/studio-admin-roles.ts +16 -0
- package/src/studio-dev-server.ts +53 -0
- package/src/type-extractor.ts +649 -186
- package/src/type-resolver.ts +457 -0
- package/tests/config.test.ts +34 -3
- package/tests/docker-postgres.test.ts +39 -0
- package/tests/normalize-admin-config.test.ts +48 -0
- package/tests/proxy-dev-app.test.ts +33 -0
- package/tests/runtime-contract.test.ts +119 -4
- package/tests/studio-admin-roles.test.ts +27 -0
- package/tests/type-extractor.test.ts +607 -23
- package/tests/type-resolver.test.ts +59 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# Type Resolution in the Schema Extractor
|
|
2
|
+
|
|
3
|
+
`type-extractor.ts` converts TypeScript type definitions into `ExtractedSchemaAst` —
|
|
4
|
+
the JSON handed to the engine binary for SQL generation and client type output.
|
|
5
|
+
|
|
6
|
+
This document explains how type names are resolved, what patterns are supported,
|
|
7
|
+
and how the three-tier fallback chain works.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## The Problem
|
|
12
|
+
|
|
13
|
+
The extractor uses the TypeScript **parser only** (`ts.createSourceFile`), not the
|
|
14
|
+
full type checker. This is fast and requires no `tsconfig.json`, but it means the
|
|
15
|
+
extractor only sees raw source text — it cannot evaluate what a type alias resolves to.
|
|
16
|
+
|
|
17
|
+
The consequence is that any indirection breaks resolution:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// Works — extractor sees "Optional" literally
|
|
21
|
+
type Post = Model<{ email: Optional<Email> }>
|
|
22
|
+
|
|
23
|
+
// Previously broken — extractor sees "Nullable", not "Optional"
|
|
24
|
+
type Nullable<T> = Optional<T>
|
|
25
|
+
type Post = Model<{ email: Nullable<Email> }>
|
|
26
|
+
|
|
27
|
+
// Previously broken — import rename
|
|
28
|
+
import { Optional as Maybe } from "@supatype/types"
|
|
29
|
+
type Post = Model<{ email: Maybe<Email> }>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Failures were silent — unknown types fell through to `{ kind: "text", pgType: "TEXT" }`
|
|
33
|
+
instead of throwing an error.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Three-Tier Resolution
|
|
38
|
+
|
|
39
|
+
Every type name encountered in a field definition is resolved through three tiers
|
|
40
|
+
in order. The first tier to succeed wins. If all three fail, an error is thrown.
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
Tier 1 — syntactic switch instant inline primitives and modifiers by name
|
|
44
|
+
Tier 2 — alias registry instant user-defined type aliases, import renames
|
|
45
|
+
Tier 3 — TypeScript checker ~300ms† conditional types, mapped types
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
† Tier 3 is **lazy** — the `ts.Program` and `TypeChecker` are only created the first
|
|
49
|
+
time a conditional or mapped type is encountered. Schemas that use only tiers 1 and 2
|
|
50
|
+
pay no cost.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Tier 1 — Syntactic Switch
|
|
55
|
+
|
|
56
|
+
The existing behaviour. The extractor walks the type reference chain and matches
|
|
57
|
+
names exactly against a hardcoded switch:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
switch (typeName) {
|
|
61
|
+
case "Optional": flags.required = false; unwrap(); continue
|
|
62
|
+
case "Unique": flags.unique = true; unwrap(); continue
|
|
63
|
+
case "PrimaryKey": flags.primaryKey = true; unwrap(); continue
|
|
64
|
+
case "UUID": return { kind: "uuid", pgType: "UUID" }
|
|
65
|
+
case "Email": return { kind: "email", pgType: "TEXT" }
|
|
66
|
+
// ... all @supatype/types primitives and modifiers
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This covers all types used inline with their canonical names.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Tier 2 — Alias Registry
|
|
75
|
+
|
|
76
|
+
Built once at startup from all source files loaded by `loadSchemaSourceFiles`.
|
|
77
|
+
Covers two sub-cases:
|
|
78
|
+
|
|
79
|
+
### 2a — Type alias declarations
|
|
80
|
+
|
|
81
|
+
Any `type X = ...` that is not a `Model<>` declaration is indexed by name:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// These all become entries in the alias registry:
|
|
85
|
+
type Nullable<T> = Optional<T>
|
|
86
|
+
type UniqueSlug = Unique<Slug<"title">>
|
|
87
|
+
type AuditId = PrimaryKey<UUID>
|
|
88
|
+
type MyEnum = "draft" | "published" | "archived"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
When the extractor encounters an unknown name, it looks it up in the registry,
|
|
92
|
+
substitutes any type parameters via text replacement, and re-enters tier 1 with
|
|
93
|
+
the resolved node.
|
|
94
|
+
|
|
95
|
+
Multi-hop aliases work because the resolution recurses:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
type A = B
|
|
99
|
+
type B = Optional<Email>
|
|
100
|
+
// A → B → Optional<Email> → resolved by tier 1
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Cycle detection via a `resolving: Set<string>` guard prevents infinite loops and
|
|
104
|
+
throws a descriptive error instead.
|
|
105
|
+
|
|
106
|
+
### 2b — Import renames
|
|
107
|
+
|
|
108
|
+
Explicit `as` renames in import statements are indexed per file:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { Optional as Maybe } from "@supatype/types"
|
|
112
|
+
import { Nullable as MaybeNull } from "./shared/field-types"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Before the tier 1 switch runs, each name is checked against the rename map for
|
|
116
|
+
the current file. `Maybe` becomes `Optional`, `MaybeNull` becomes `Nullable`
|
|
117
|
+
(which is then resolved by 2a).
|
|
118
|
+
|
|
119
|
+
### File loading
|
|
120
|
+
|
|
121
|
+
`loadSchemaSourceFiles` follows both `export` declarations (existing) and local
|
|
122
|
+
`import` declarations (new), ensuring that files referenced via import are loaded
|
|
123
|
+
into the source file set and their aliases are available in the registry.
|
|
124
|
+
|
|
125
|
+
Only relative specifiers (`.`-prefixed) are followed. Bare specifiers and scoped
|
|
126
|
+
packages (`@supatype/types`, `node_modules/*`) are not loaded — their exported
|
|
127
|
+
names are already covered by the tier 1 switch.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Tier 3 — TypeScript Checker
|
|
132
|
+
|
|
133
|
+
Required for types that cannot be evaluated syntactically:
|
|
134
|
+
|
|
135
|
+
- **Conditional types**: `T extends U ? A : B` — requires evaluating the constraint
|
|
136
|
+
- **Mapped types**: `{ [K in keyof T]: F<T[K]> }` — requires enumerating `keyof T`
|
|
137
|
+
|
|
138
|
+
### How it works
|
|
139
|
+
|
|
140
|
+
1. A lazy `ts.Program` is created from the already-loaded source files using a
|
|
141
|
+
custom `CompilerHost` that serves them from memory (no disk re-reads).
|
|
142
|
+
|
|
143
|
+
2. Because our lightweight parse tree and the Program's parse tree are different
|
|
144
|
+
objects (even for the same file), the node with the unresolvable type is located
|
|
145
|
+
in the Program's source file by matching `pos`/`end` character positions.
|
|
146
|
+
|
|
147
|
+
3. `checker.getTypeAtLocation(programNode)` resolves the type fully.
|
|
148
|
+
|
|
149
|
+
4. `checker.typeToString(type, ..., TypeFormatFlags.UseAliasDefinedOutsideCurrentScope)`
|
|
150
|
+
converts it back to a string with **alias names preserved** — so `Optional<Email>`
|
|
151
|
+
stays as `Optional<Email>` rather than expanding to the underlying branded
|
|
152
|
+
intersection type.
|
|
153
|
+
|
|
154
|
+
5. The string is re-parsed via `ts.createSourceFile` into a proper TypeNode with
|
|
155
|
+
valid `pos`/`end` values (so downstream `getText()` calls work).
|
|
156
|
+
|
|
157
|
+
6. The resolved TypeNode is fed back into the tier 1 switch.
|
|
158
|
+
|
|
159
|
+
### When tier 3 fires
|
|
160
|
+
|
|
161
|
+
- A field type is directly a conditional or mapped expression
|
|
162
|
+
- A tier 2 alias body contains a conditional or mapped type (detected by
|
|
163
|
+
`needsChecker()` before text substitution is attempted — the original node,
|
|
164
|
+
which has valid source positions, is passed to the checker instead)
|
|
165
|
+
- The `Model<>` fields argument is a mapped type alias:
|
|
166
|
+
```typescript
|
|
167
|
+
type AllOptional<T> = { [K in keyof T]: Optional<T[K]> }
|
|
168
|
+
type Post = Model<AllOptional<{ email: Email; name: Text }>>
|
|
169
|
+
```
|
|
170
|
+
This is handled in `unwrapModelFields`, which also participates in the
|
|
171
|
+
three-tier chain.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## What Is Supported
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// Tier 1 — inline, canonical names
|
|
179
|
+
email: Optional<Email>
|
|
180
|
+
slug: Unique<Slug<"title">>
|
|
181
|
+
id: PrimaryKey<UUID>
|
|
182
|
+
|
|
183
|
+
// Tier 2a — simple alias
|
|
184
|
+
type Nullable<T> = Optional<T>
|
|
185
|
+
type UniqueSlug = Unique<Slug<"title">>
|
|
186
|
+
email: Nullable<Email>
|
|
187
|
+
slug: UniqueSlug
|
|
188
|
+
|
|
189
|
+
// Tier 2a — multi-hop
|
|
190
|
+
type A = B
|
|
191
|
+
type B = Nullable<Email>
|
|
192
|
+
email: A
|
|
193
|
+
|
|
194
|
+
// Tier 2b — import rename of primitive
|
|
195
|
+
import { Optional as Maybe } from "@supatype/types"
|
|
196
|
+
email: Maybe<Email>
|
|
197
|
+
|
|
198
|
+
// Tier 2b — import rename of local alias
|
|
199
|
+
import { Nullable as MaybeNull } from "./field-types"
|
|
200
|
+
email: MaybeNull<Email>
|
|
201
|
+
|
|
202
|
+
// Tier 2b — cross-file alias (no rename)
|
|
203
|
+
// helpers.ts: export type Nullable<T> = Optional<T>
|
|
204
|
+
import { Nullable } from "./helpers"
|
|
205
|
+
email: Nullable<Email>
|
|
206
|
+
|
|
207
|
+
// Tier 3 — conditional type
|
|
208
|
+
type NullableStr<T> = T extends string ? Optional<T> : T
|
|
209
|
+
email: NullableStr<Email>
|
|
210
|
+
|
|
211
|
+
// Tier 3 — mapped type as fields object
|
|
212
|
+
type AllOptional<T> = { [K in keyof T]: Optional<T[K]> }
|
|
213
|
+
type Post = Model<AllOptional<{ email: Email; name: Text }>>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## What Is Not Supported
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// Imports from node_modules other than @supatype/types
|
|
222
|
+
import { SomeHelper } from "some-library"
|
|
223
|
+
|
|
224
|
+
// Conditional / mapped types in alias bodies that reference
|
|
225
|
+
// symbols only available in node_modules (other than @supatype/types)
|
|
226
|
+
|
|
227
|
+
// TypeScript utility types used as field types
|
|
228
|
+
email: NonNullable<string> // error — not a @supatype/types primitive
|
|
229
|
+
|
|
230
|
+
// Namespace-qualified names
|
|
231
|
+
email: Types.Optional<Email> // error — only identifier references are resolved
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Error Behaviour
|
|
237
|
+
|
|
238
|
+
Unknown types now **throw** instead of silently falling back to
|
|
239
|
+
`{ kind: "text", pgType: "TEXT" }`:
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
Error: Unknown Supatype type "SomeType" in field "email".
|
|
243
|
+
If this is a type alias, confirm the file defining it is reachable
|
|
244
|
+
from your schema entry point.
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Cycles in alias chains throw:
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
Error: Field "email": circular alias chain detected resolving "A".
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Unresolvable conditional/mapped types throw:
|
|
254
|
+
|
|
255
|
+
```
|
|
256
|
+
Error: Field "email": could not resolve conditional/mapped type via type checker.
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Data Structures
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// One entry per non-Model type alias declaration across all loaded source files
|
|
265
|
+
type AliasEntry = {
|
|
266
|
+
typeParams: string[] // ["T"] for Nullable<T> = Optional<T>
|
|
267
|
+
body: ts.TypeNode // the RHS of the declaration
|
|
268
|
+
sourceFile: ts.SourceFile // getText() context for body
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Rename map: sf.fileName → (localName → canonicalName)
|
|
272
|
+
// Only populated for explicit `import { X as Y }` renames
|
|
273
|
+
type ImportRenameMap = Map<string, Map<string, string>>
|
|
274
|
+
|
|
275
|
+
// Passed to every resolution function; checker is lazy
|
|
276
|
+
type ResolveContext = {
|
|
277
|
+
aliasRegistry: Map<string, AliasEntry>
|
|
278
|
+
renameMap: ImportRenameMap
|
|
279
|
+
getChecker: () => CheckerContext
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
type CheckerContext = {
|
|
283
|
+
program: ts.Program
|
|
284
|
+
checker: ts.TypeChecker
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Adding a New Tier 1 Primitive
|
|
291
|
+
|
|
292
|
+
When a new type is added to `@supatype/types`, add a `case` to the `switch` in
|
|
293
|
+
`parseScalarType`. No other changes are needed — tier 2 and tier 3 handle
|
|
294
|
+
aliases and compositions of it automatically.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawn the frontend dev server during `supatype dev` when app.mode is proxy.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
6
|
+
import { join } from "node:path"
|
|
7
|
+
import { detectPackageManager } from "./framework.js"
|
|
8
|
+
import { ProcessManager } from "../process-manager.js"
|
|
9
|
+
import { projectRootFromConfig, type SupatypeProjectConfig } from "../project-config.js"
|
|
10
|
+
|
|
11
|
+
const DEFAULT_PROXY_DEV_SCRIPT = "start"
|
|
12
|
+
|
|
13
|
+
/** package.json script name to run (only when app.mode is proxy). */
|
|
14
|
+
export function resolveProxyDevScript(config: SupatypeProjectConfig): string | null {
|
|
15
|
+
if (config.app?.mode !== "proxy") return null
|
|
16
|
+
const script = config.app.start?.trim()
|
|
17
|
+
return script && script.length > 0 ? script : DEFAULT_PROXY_DEV_SCRIPT
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Start the configured package.json script for proxy dev.
|
|
22
|
+
* Returns null when not in proxy mode or when the script is missing.
|
|
23
|
+
*/
|
|
24
|
+
export function startProxyDevApp(
|
|
25
|
+
cwd: string,
|
|
26
|
+
config: SupatypeProjectConfig,
|
|
27
|
+
pidDir: string,
|
|
28
|
+
): ProcessManager | null {
|
|
29
|
+
const script = resolveProxyDevScript(config)
|
|
30
|
+
if (!script) return null
|
|
31
|
+
|
|
32
|
+
const appDir = projectRootFromConfig(config, cwd)
|
|
33
|
+
const pkgPath = join(appDir, "package.json")
|
|
34
|
+
if (!existsSync(pkgPath)) {
|
|
35
|
+
console.warn(`[supatype] app.mode=proxy but no package.json at ${appDir}; skipping dev app`)
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as {
|
|
40
|
+
scripts?: Record<string, string>
|
|
41
|
+
}
|
|
42
|
+
if (!pkg.scripts?.[script]) {
|
|
43
|
+
console.warn(
|
|
44
|
+
`[supatype] app.mode=proxy: package.json has no "${script}" script.\n` +
|
|
45
|
+
` Add "scripts.${script}" or set app.start in supatype.config.ts`,
|
|
46
|
+
)
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pm = detectPackageManager(appDir)
|
|
51
|
+
const bin = pm
|
|
52
|
+
const args = pm === "yarn" ? [script] : ["run", script]
|
|
53
|
+
// pnpm/npm/yarn are .cmd shims on Windows — spawn via shell so PATH resolution works.
|
|
54
|
+
const useShell = process.platform === "win32"
|
|
55
|
+
|
|
56
|
+
console.log(`[supatype] Proxy mode: running ${bin} ${args.join(" ")} (${appDir})`)
|
|
57
|
+
|
|
58
|
+
const manager = new ProcessManager(bin, args, {
|
|
59
|
+
label: "app",
|
|
60
|
+
pidDir,
|
|
61
|
+
cwd: appDir,
|
|
62
|
+
colour: "\x1b[33m",
|
|
63
|
+
shell: useShell,
|
|
64
|
+
})
|
|
65
|
+
manager.start()
|
|
66
|
+
return manager
|
|
67
|
+
}
|
package/src/binary-cache.ts
CHANGED
|
@@ -39,6 +39,17 @@ import { releasePublicKey } from "./release-public-key.js"
|
|
|
39
39
|
*/
|
|
40
40
|
export const VERSION_PIN_LOCAL = "local"
|
|
41
41
|
|
|
42
|
+
/** True if `overrides.engine` points at a local engine binary (contributor dev). */
|
|
43
|
+
export function hasEngineOverride(config: SupatypeProjectConfig): boolean {
|
|
44
|
+
const path = config.overrides?.engine
|
|
45
|
+
return typeof path === "string" && path.trim() !== ""
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function hasStudioOverride(config: SupatypeProjectConfig): boolean {
|
|
49
|
+
const path = config.overrides?.studio
|
|
50
|
+
return typeof path === "string" && path.trim() !== ""
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
/** True if `overrides` contains any non-empty string path (contributor local builds). */
|
|
43
54
|
export function hasMeaningfulOverrides(config: SupatypeProjectConfig): boolean {
|
|
44
55
|
const o = config.overrides
|
|
@@ -281,6 +292,15 @@ export async function resolveBinary(
|
|
|
281
292
|
* 2. If SUPATYPE_RELEASE_PUBLIC_KEY is set: verify minisign signature.
|
|
282
293
|
* 3. Verify SHA256 of downloaded binary against signed checksum.
|
|
283
294
|
*/
|
|
295
|
+
/** Download if missing or invalid; return cached path for the given platform. */
|
|
296
|
+
export async function ensureCachedBinary(
|
|
297
|
+
component: Component,
|
|
298
|
+
version: string,
|
|
299
|
+
platform: PlatformId,
|
|
300
|
+
): Promise<string> {
|
|
301
|
+
return download(component, version, platform)
|
|
302
|
+
}
|
|
303
|
+
|
|
284
304
|
export async function download(
|
|
285
305
|
component: Component,
|
|
286
306
|
version: string,
|
package/src/commands/cloud.ts
CHANGED
|
@@ -7,6 +7,8 @@ interface CloudConfig {
|
|
|
7
7
|
apiUrl: string
|
|
8
8
|
token: string
|
|
9
9
|
projectSlug?: string
|
|
10
|
+
/** Organisation UUID — required for schema routes (`X-Org-Id`). */
|
|
11
|
+
orgId?: string
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export function loadCloudConfig(cwd: string): CloudConfig | null {
|
|
@@ -25,12 +27,16 @@ function saveCloudConfig(cwd: string, config: CloudConfig): void {
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
async function cloudFetch<T>(config: CloudConfig, method: string, path: string, body?: unknown): Promise<T> {
|
|
30
|
+
const headers: Record<string, string> = {
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
Authorization: `Bearer ${config.token}`,
|
|
33
|
+
}
|
|
34
|
+
if (config.orgId) {
|
|
35
|
+
headers["X-Org-Id"] = config.orgId
|
|
36
|
+
}
|
|
28
37
|
const res = await fetch(`${config.apiUrl}/api/v1${path}`, {
|
|
29
38
|
method,
|
|
30
|
-
headers
|
|
31
|
-
"Content-Type": "application/json",
|
|
32
|
-
Authorization: `Bearer ${config.token}`,
|
|
33
|
-
},
|
|
39
|
+
headers,
|
|
34
40
|
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
35
41
|
})
|
|
36
42
|
|
|
@@ -41,21 +47,28 @@ async function cloudFetch<T>(config: CloudConfig, method: string, path: string,
|
|
|
41
47
|
return json.data as T
|
|
42
48
|
}
|
|
43
49
|
|
|
50
|
+
/** True when `.supatype/cloud.json` exists with a linked project slug. */
|
|
51
|
+
export function isCloudLinked(cwd: string): boolean {
|
|
52
|
+
const cfg = loadCloudConfig(cwd)
|
|
53
|
+
return Boolean(cfg?.projectSlug && cfg.token)
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
/**
|
|
45
|
-
* Push schema AST to the linked cloud project (
|
|
46
|
-
*
|
|
57
|
+
* Push schema AST to the linked cloud project (`POST /api/v1/projects/:ref/schema/push`).
|
|
58
|
+
* Credentials stay server-side; only AST is sent.
|
|
47
59
|
*/
|
|
48
|
-
export async function
|
|
49
|
-
cwd: string,
|
|
50
|
-
environment: string,
|
|
51
|
-
): Promise<void> {
|
|
60
|
+
export async function pushSchemaToLinkedProject(cwd: string, opts?: { force?: boolean }): Promise<void> {
|
|
52
61
|
const config = loadCloudConfig(cwd)
|
|
53
62
|
if (!config?.projectSlug) {
|
|
54
63
|
console.error("Not linked to a cloud project. Run: supatype link")
|
|
55
64
|
process.exit(1)
|
|
56
65
|
}
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
if (!config.orgId) {
|
|
67
|
+
console.error(
|
|
68
|
+
"Missing orgId in .supatype/cloud.json. Re-run: supatype link --project <slug> (after cloud login).",
|
|
69
|
+
)
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
59
72
|
|
|
60
73
|
const { loadConfig: loadAppConfig, loadSchemaAst } = await import("../config.js")
|
|
61
74
|
const { schemaPathFromProject } = await import("../project-config.js")
|
|
@@ -63,25 +76,19 @@ export async function deploySchemaToLinkedProject(
|
|
|
63
76
|
const appConfig = loadAppConfig(cwd)
|
|
64
77
|
const ast = loadSchemaAst(schemaPathFromProject(appConfig, cwd), cwd)
|
|
65
78
|
|
|
66
|
-
|
|
67
|
-
const schemaHash = createHash("sha256").update(JSON.stringify(ast)).digest("hex").slice(0, 16)
|
|
79
|
+
console.log(`Pushing schema to cloud project ${config.projectSlug}...`)
|
|
68
80
|
|
|
69
|
-
const
|
|
70
|
-
id: string; status: string; errorMessage?: string
|
|
71
|
-
}>(config, "POST", `/projects/${config.projectSlug}/deploy`, {
|
|
72
|
-
environment,
|
|
73
|
-
schemaHash,
|
|
81
|
+
const result = await cloudFetch<{ message?: string }>(config, "POST", `/projects/${config.projectSlug}/schema/push`, {
|
|
74
82
|
ast,
|
|
83
|
+
force: opts?.force ?? true,
|
|
75
84
|
})
|
|
76
85
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
console.log(`\nDeployment ${deployment.status} (${deployment.id})`)
|
|
84
|
-
}
|
|
86
|
+
console.log(result.message ?? "Schema push completed.")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @deprecated Use pushSchemaToLinkedProject — kept for deploy command alias */
|
|
90
|
+
export async function deploySchemaToLinkedProject(cwd: string, _environment: string): Promise<void> {
|
|
91
|
+
await pushSchemaToLinkedProject(cwd)
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
function prompt(question: string): Promise<string> {
|
|
@@ -116,9 +123,10 @@ export function registerCloud(program: Command): void {
|
|
|
116
123
|
|
|
117
124
|
if (opts.project) {
|
|
118
125
|
config.projectSlug = opts.project
|
|
126
|
+
const one = await cloudFetch<{ slug: string; orgId: string }>(config, "GET", `/projects/${opts.project}`)
|
|
127
|
+
config.orgId = one.orgId
|
|
119
128
|
} else {
|
|
120
|
-
|
|
121
|
-
const projects = await cloudFetch<Array<{ slug: string; name: string; status: string; tier: string }>>(
|
|
129
|
+
const projects = await cloudFetch<Array<{ slug: string; name: string; status: string; tier: string; orgId: string }>>(
|
|
122
130
|
config, "GET", "/projects",
|
|
123
131
|
)
|
|
124
132
|
if (projects.length === 0) {
|
|
@@ -137,7 +145,9 @@ export function registerCloud(program: Command): void {
|
|
|
137
145
|
console.error("Invalid selection.")
|
|
138
146
|
process.exit(1)
|
|
139
147
|
}
|
|
140
|
-
|
|
148
|
+
const picked = projects[idx]!
|
|
149
|
+
config.projectSlug = picked.slug
|
|
150
|
+
config.orgId = picked.orgId
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
saveCloudConfig(cwd, config)
|
package/src/commands/deploy.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { existsSync, readdirSync, statSync, createReadStream } from "node:fs"
|
|
|
16
16
|
import { join } from "node:path"
|
|
17
17
|
import { loadConfig, loadSchemaAst } from "../config.js"
|
|
18
18
|
import { connectionString, schemaPathFromProject } from "../project-config.js"
|
|
19
|
-
import { deploySchemaToLinkedProject, loadCloudConfig } from "./cloud.js"
|
|
19
|
+
import { deploySchemaToLinkedProject, loadCloudConfig, pushSchemaToLinkedProject } from "./cloud.js"
|
|
20
20
|
import { ensureEngine, engineRequest, type DiffResult } from "../engine-client.js"
|
|
21
21
|
import { resolveAppConfig, validateStaticMode, validateBuildOutput, detectPackageManager } from "../app/framework.js"
|
|
22
22
|
import { TIER_LIMITS, type Tier } from "./deploy-types.js"
|
|
@@ -93,23 +93,8 @@ export function registerDeploy(program: Command): void {
|
|
|
93
93
|
console.log("Schema is up to date.")
|
|
94
94
|
}
|
|
95
95
|
} else if (cloudCfg?.projectSlug) {
|
|
96
|
-
console.log("=== Schema Push ===")
|
|
97
|
-
|
|
98
|
-
const apiUrl = cloudCfg.apiUrl || "https://api.supatype.com"
|
|
99
|
-
const token = cloudCfg.token || process.env["SUPATYPE_ACCESS_TOKEN"] || ""
|
|
100
|
-
|
|
101
|
-
const res = await fetch(`${apiUrl}/platform/v1/projects/${cloudCfg.projectSlug}/schema/push`, {
|
|
102
|
-
method: "POST",
|
|
103
|
-
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
104
|
-
body: JSON.stringify({ ast }),
|
|
105
|
-
})
|
|
106
|
-
if (!res.ok) {
|
|
107
|
-
const body = await res.text()
|
|
108
|
-
console.error(`Schema push failed: ${res.status} ${body}`)
|
|
109
|
-
process.exit(1)
|
|
110
|
-
}
|
|
111
|
-
const pushData = await res.json() as { operations?: unknown[]; message?: string }
|
|
112
|
-
console.log(pushData.message ?? `Schema changes applied (${pushData.operations?.length ?? 0} operations).`)
|
|
96
|
+
console.log("=== Schema Push (cloud) ===")
|
|
97
|
+
await pushSchemaToLinkedProject(cwd, { force: opts.yes ?? true })
|
|
113
98
|
} else {
|
|
114
99
|
console.error(
|
|
115
100
|
"Not linked to Supatype Cloud. Run: supatype link\n" +
|