@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,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST v2 wire format — canonical types and emitters.
|
|
3
|
+
* Parsers build {@link ParsedField}; only `emitField` / `emitModel` / `emitSchema` produce JSON.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const AST_VERSION = 2 as const
|
|
7
|
+
|
|
8
|
+
export type DefaultAst =
|
|
9
|
+
| { kind: "value"; value: string | number | boolean | null }
|
|
10
|
+
| { kind: "now" }
|
|
11
|
+
| { kind: "genRandomUuid" }
|
|
12
|
+
| { kind: "expression"; expr: string }
|
|
13
|
+
|
|
14
|
+
export interface DbFieldAnnotations {
|
|
15
|
+
pgType?: string
|
|
16
|
+
unique?: boolean
|
|
17
|
+
index?: boolean
|
|
18
|
+
foreignKey?: string
|
|
19
|
+
serverGenerated?: boolean
|
|
20
|
+
elementType?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PlatformFieldAnnotations {
|
|
24
|
+
editor?: string
|
|
25
|
+
readOnly?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FieldAnnotations {
|
|
29
|
+
db?: DbFieldAnnotations
|
|
30
|
+
platform?: PlatformFieldAnnotations
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Kernel facts only — never db/platform keys at this layer. */
|
|
34
|
+
export interface KernelFieldFacts {
|
|
35
|
+
required?: boolean
|
|
36
|
+
primaryKey?: boolean
|
|
37
|
+
default?: DefaultAst
|
|
38
|
+
localized?: boolean
|
|
39
|
+
cardinality?: string
|
|
40
|
+
target?: string
|
|
41
|
+
values?: string[]
|
|
42
|
+
from?: string
|
|
43
|
+
sources?: string[]
|
|
44
|
+
template?: string
|
|
45
|
+
bucket?: string
|
|
46
|
+
accessMode?: string
|
|
47
|
+
geoType?: string
|
|
48
|
+
srid?: number
|
|
49
|
+
dimensions?: number
|
|
50
|
+
blocks?: BlockDefinitionAst[]
|
|
51
|
+
check?: string
|
|
52
|
+
precision?: number
|
|
53
|
+
scale?: number
|
|
54
|
+
references?: string
|
|
55
|
+
through?: string
|
|
56
|
+
onDelete?: string
|
|
57
|
+
onUpdate?: string
|
|
58
|
+
uniqueFk?: boolean
|
|
59
|
+
plugin?: string
|
|
60
|
+
fieldType?: string
|
|
61
|
+
tsType?: string
|
|
62
|
+
index?: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Internal parse result — not serialized. */
|
|
66
|
+
export interface ParsedField {
|
|
67
|
+
kind: string
|
|
68
|
+
kernel: KernelFieldFacts
|
|
69
|
+
db: DbFieldAnnotations
|
|
70
|
+
platform: PlatformFieldAnnotations
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type FieldAstV2 = {
|
|
74
|
+
kind: string
|
|
75
|
+
annotations?: FieldAnnotations
|
|
76
|
+
} & Record<string, unknown>
|
|
77
|
+
|
|
78
|
+
export interface BlockDefinitionAst {
|
|
79
|
+
name: string
|
|
80
|
+
label?: string
|
|
81
|
+
icon?: string
|
|
82
|
+
fields: Record<string, FieldAstV2>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ModelAstV2 {
|
|
86
|
+
name: string
|
|
87
|
+
fields: Record<string, FieldAstV2>
|
|
88
|
+
options: Record<string, unknown>
|
|
89
|
+
annotations: {
|
|
90
|
+
db: { tableName: string; indexes: unknown[] }
|
|
91
|
+
platform: { access: Record<string, unknown> }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ExtractedStorageBucketAst {
|
|
96
|
+
id: string
|
|
97
|
+
public: boolean
|
|
98
|
+
accessMode?: "public" | "private" | "custom"
|
|
99
|
+
allowedMimeTypes?: string[]
|
|
100
|
+
fileSizeLimit?: number
|
|
101
|
+
access?: Record<string, unknown>
|
|
102
|
+
s3BucketPolicy?: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ExtractedSchemaAstV2 {
|
|
106
|
+
astVersion: typeof AST_VERSION
|
|
107
|
+
models: ModelAstV2[]
|
|
108
|
+
storageBuckets?: ExtractedStorageBucketAst[]
|
|
109
|
+
locales?: string[]
|
|
110
|
+
defaultLocale?: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const DEFAULT_DB_BY_KIND: Record<string, Partial<DbFieldAnnotations>> = {
|
|
114
|
+
text: { pgType: "TEXT" },
|
|
115
|
+
richText: { pgType: "JSONB" },
|
|
116
|
+
integer: { pgType: "INTEGER" },
|
|
117
|
+
smallInt: { pgType: "SMALLINT" },
|
|
118
|
+
bigInt: { pgType: "BIGINT" },
|
|
119
|
+
float: { pgType: "DOUBLE PRECISION" },
|
|
120
|
+
boolean: { pgType: "BOOLEAN" },
|
|
121
|
+
datetime: { pgType: "TIMESTAMPTZ" },
|
|
122
|
+
date: { pgType: "DATE" },
|
|
123
|
+
timestamp: { pgType: "TIMESTAMP WITH TIME ZONE" },
|
|
124
|
+
uuid: { pgType: "UUID" },
|
|
125
|
+
email: { pgType: "TEXT" },
|
|
126
|
+
url: { pgType: "TEXT" },
|
|
127
|
+
slug: { pgType: "TEXT" },
|
|
128
|
+
enum: { pgType: "TEXT" },
|
|
129
|
+
json: { pgType: "JSONB" },
|
|
130
|
+
decimal: { pgType: "TEXT" },
|
|
131
|
+
bytes: { pgType: "BYTEA" },
|
|
132
|
+
serial: { pgType: "SERIAL" },
|
|
133
|
+
bigSerial: { pgType: "BIGSERIAL" },
|
|
134
|
+
money: { pgType: "TEXT" },
|
|
135
|
+
ip: { pgType: "TEXT" },
|
|
136
|
+
cidr: { pgType: "TEXT" },
|
|
137
|
+
macaddr: { pgType: "TEXT" },
|
|
138
|
+
xml: { pgType: "TEXT" },
|
|
139
|
+
tsQuery: { pgType: "TEXT" },
|
|
140
|
+
tsVector: { pgType: "TEXT" },
|
|
141
|
+
color: { pgType: "TEXT" },
|
|
142
|
+
array: { pgType: "ARRAY" },
|
|
143
|
+
image: { pgType: "JSONB" },
|
|
144
|
+
file: { pgType: "JSONB" },
|
|
145
|
+
geo: { pgType: "GEOGRAPHY" },
|
|
146
|
+
vector: { pgType: "VECTOR" },
|
|
147
|
+
blocks: { pgType: "JSONB" },
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const DEFAULT_PLATFORM_BY_KIND: Record<string, Partial<PlatformFieldAnnotations>> = {
|
|
151
|
+
richText: { editor: "rich" },
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const COMPOSITE_KINDS = new Set(["timestamps", "publishable", "softDelete"])
|
|
155
|
+
|
|
156
|
+
function hasKeys(obj: Record<string, unknown>): boolean {
|
|
157
|
+
return Object.keys(obj).length > 0
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function stripUndefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
|
|
161
|
+
const out: Record<string, unknown> = {}
|
|
162
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
163
|
+
if (v !== undefined) out[k] = v
|
|
164
|
+
}
|
|
165
|
+
return out as Partial<T>
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Start a parsed field with kind defaults for db/platform namespaces. */
|
|
169
|
+
export function defaultPgTypeForKind(kind: string): string {
|
|
170
|
+
return DEFAULT_DB_BY_KIND[kind]?.pgType ?? "TEXT"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Flat wire shape for fields nested inside `blocks` definitions (engine FieldAst serde). */
|
|
174
|
+
export function emitBlockNestedField(field: FieldAstV2): FieldAstV2 {
|
|
175
|
+
const annotations = (field.annotations ?? {}) as FieldAnnotations
|
|
176
|
+
const db = annotations.db ?? {}
|
|
177
|
+
const platform = annotations.platform ?? {}
|
|
178
|
+
|
|
179
|
+
const wire: FieldAstV2 = {
|
|
180
|
+
kind: field.kind,
|
|
181
|
+
pgType: db.pgType ?? defaultPgTypeForKind(String(field.kind)),
|
|
182
|
+
required: field.required ?? false,
|
|
183
|
+
unique: db.unique ?? false,
|
|
184
|
+
localized: field.localized ?? false,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (platform.readOnly) wire.readOnly = true
|
|
188
|
+
if (field.primaryKey) wire.primaryKey = true
|
|
189
|
+
if (field.default !== undefined) wire.default = field.default
|
|
190
|
+
if (field.from !== undefined) wire.from = field.from
|
|
191
|
+
if (field.values !== undefined) wire.values = field.values
|
|
192
|
+
if (field.bucket !== undefined) wire.bucket = field.bucket
|
|
193
|
+
if (field.accessMode !== undefined) wire.accessMode = field.accessMode
|
|
194
|
+
if (field.geoType !== undefined) wire.geoType = field.geoType
|
|
195
|
+
if (field.srid !== undefined) wire.srid = field.srid
|
|
196
|
+
if (field.dimensions !== undefined) wire.dimensions = field.dimensions
|
|
197
|
+
if (field.check !== undefined) wire.check = field.check
|
|
198
|
+
if (field.precision !== undefined) wire.precision = field.precision
|
|
199
|
+
if (field.scale !== undefined) wire.scale = field.scale
|
|
200
|
+
if (field.sources !== undefined) wire.sources = field.sources
|
|
201
|
+
if (field.template !== undefined) wire.template = field.template
|
|
202
|
+
if (field.plugin !== undefined) wire.plugin = field.plugin
|
|
203
|
+
if (field.fieldType !== undefined) wire.fieldType = field.fieldType
|
|
204
|
+
if (field.tsType !== undefined) wire.tsType = field.tsType
|
|
205
|
+
|
|
206
|
+
return wire
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function scalar(
|
|
210
|
+
kind: string,
|
|
211
|
+
extra?: {
|
|
212
|
+
kernel?: Partial<KernelFieldFacts>
|
|
213
|
+
db?: Partial<DbFieldAnnotations>
|
|
214
|
+
platform?: Partial<PlatformFieldAnnotations>
|
|
215
|
+
},
|
|
216
|
+
): ParsedField {
|
|
217
|
+
return {
|
|
218
|
+
kind,
|
|
219
|
+
kernel: { ...(extra?.kernel ?? {}) },
|
|
220
|
+
db: { ...DEFAULT_DB_BY_KIND[kind], ...stripUndefined(extra?.db ?? {}) },
|
|
221
|
+
platform: { ...DEFAULT_PLATFORM_BY_KIND[kind], ...stripUndefined(extra?.platform ?? {}) },
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function emitField(parsed: ParsedField): FieldAstV2 {
|
|
226
|
+
if (COMPOSITE_KINDS.has(parsed.kind)) {
|
|
227
|
+
return { kind: parsed.kind }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const db = stripUndefined({
|
|
231
|
+
...DEFAULT_DB_BY_KIND[parsed.kind],
|
|
232
|
+
...parsed.db,
|
|
233
|
+
}) as DbFieldAnnotations
|
|
234
|
+
|
|
235
|
+
const platform = stripUndefined({
|
|
236
|
+
...DEFAULT_PLATFORM_BY_KIND[parsed.kind],
|
|
237
|
+
...parsed.platform,
|
|
238
|
+
}) as PlatformFieldAnnotations
|
|
239
|
+
|
|
240
|
+
const wire: FieldAstV2 = { kind: parsed.kind }
|
|
241
|
+
|
|
242
|
+
const kernel = parsed.kernel
|
|
243
|
+
if (kernel.required !== undefined) wire.required = kernel.required
|
|
244
|
+
if (kernel.primaryKey) wire.primaryKey = true
|
|
245
|
+
if (kernel.default !== undefined) wire.default = kernel.default
|
|
246
|
+
if (kernel.localized) wire.localized = true
|
|
247
|
+
if (kernel.cardinality !== undefined) wire.cardinality = kernel.cardinality
|
|
248
|
+
if (kernel.target !== undefined) wire.target = kernel.target
|
|
249
|
+
if (kernel.values !== undefined) wire.values = kernel.values
|
|
250
|
+
if (kernel.from !== undefined) wire.from = kernel.from
|
|
251
|
+
if (kernel.sources !== undefined && kernel.sources.length > 0) wire.sources = kernel.sources
|
|
252
|
+
if (kernel.template !== undefined) wire.template = kernel.template
|
|
253
|
+
if (kernel.bucket !== undefined) wire.bucket = kernel.bucket
|
|
254
|
+
if (kernel.accessMode !== undefined) wire.accessMode = kernel.accessMode
|
|
255
|
+
if (kernel.geoType !== undefined) wire.geoType = kernel.geoType
|
|
256
|
+
if (kernel.srid !== undefined) wire.srid = kernel.srid
|
|
257
|
+
if (kernel.dimensions !== undefined) wire.dimensions = kernel.dimensions
|
|
258
|
+
if (kernel.blocks !== undefined) {
|
|
259
|
+
wire.blocks = kernel.blocks.map((blockDef) => ({
|
|
260
|
+
...blockDef,
|
|
261
|
+
fields: Object.fromEntries(
|
|
262
|
+
Object.entries(blockDef.fields).map(([name, nested]) => [
|
|
263
|
+
name,
|
|
264
|
+
emitBlockNestedField(nested),
|
|
265
|
+
]),
|
|
266
|
+
),
|
|
267
|
+
}))
|
|
268
|
+
}
|
|
269
|
+
if (kernel.check !== undefined) wire.check = kernel.check
|
|
270
|
+
if (kernel.precision !== undefined) wire.precision = kernel.precision
|
|
271
|
+
if (kernel.scale !== undefined) wire.scale = kernel.scale
|
|
272
|
+
if (kernel.references !== undefined) wire.references = kernel.references
|
|
273
|
+
if (kernel.through !== undefined) wire.through = kernel.through
|
|
274
|
+
if (kernel.onDelete !== undefined) wire.onDelete = kernel.onDelete
|
|
275
|
+
if (kernel.onUpdate !== undefined) wire.onUpdate = kernel.onUpdate
|
|
276
|
+
if (kernel.uniqueFk) wire.uniqueFk = true
|
|
277
|
+
if (kernel.plugin !== undefined) wire.plugin = kernel.plugin
|
|
278
|
+
if (kernel.fieldType !== undefined) wire.fieldType = kernel.fieldType
|
|
279
|
+
if (kernel.tsType !== undefined) wire.tsType = kernel.tsType
|
|
280
|
+
if (parsed.kind === "blocks" && kernel.index !== undefined) wire.index = kernel.index
|
|
281
|
+
|
|
282
|
+
const annotations: FieldAnnotations = {}
|
|
283
|
+
if (hasKeys(db as Record<string, unknown>)) annotations.db = db
|
|
284
|
+
if (hasKeys(platform as Record<string, unknown>)) annotations.platform = platform
|
|
285
|
+
if (hasKeys(annotations as Record<string, unknown>)) wire.annotations = annotations
|
|
286
|
+
|
|
287
|
+
return wire
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function emitModel(
|
|
291
|
+
name: string,
|
|
292
|
+
fields: Record<string, FieldAstV2>,
|
|
293
|
+
options: Record<string, unknown>,
|
|
294
|
+
tableName: string,
|
|
295
|
+
access: Record<string, unknown>,
|
|
296
|
+
): ModelAstV2 {
|
|
297
|
+
return {
|
|
298
|
+
name,
|
|
299
|
+
fields,
|
|
300
|
+
options,
|
|
301
|
+
annotations: {
|
|
302
|
+
db: { tableName, indexes: [] },
|
|
303
|
+
platform: { access },
|
|
304
|
+
},
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function emitSchema(
|
|
309
|
+
models: ModelAstV2[],
|
|
310
|
+
extras?: {
|
|
311
|
+
storageBuckets?: ExtractedSchemaAstV2["storageBuckets"]
|
|
312
|
+
locales?: string[]
|
|
313
|
+
defaultLocale?: string
|
|
314
|
+
},
|
|
315
|
+
): ExtractedSchemaAstV2 {
|
|
316
|
+
return {
|
|
317
|
+
astVersion: AST_VERSION,
|
|
318
|
+
models,
|
|
319
|
+
...(extras?.storageBuckets !== undefined &&
|
|
320
|
+
extras.storageBuckets.length > 0 && { storageBuckets: extras.storageBuckets }),
|
|
321
|
+
...(extras?.locales !== undefined && extras.locales.length > 0 && { locales: extras.locales }),
|
|
322
|
+
...(extras?.defaultLocale !== undefined && { defaultLocale: extras.defaultLocale }),
|
|
323
|
+
}
|
|
324
|
+
}
|
package/src/self-host-compose.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
2
2
|
import { dirname, join, relative, resolve } from "node:path"
|
|
3
3
|
import { spawnSync } from "node:child_process"
|
|
4
|
-
import type
|
|
4
|
+
import { preferredFunctionsPathFromProject, type SupatypeProjectConfig } from "./project-config.js"
|
|
5
|
+
import { hasEngineOverride, hasStudioOverride } from "./binary-cache.js"
|
|
5
6
|
import { buildKongDeclarative } from "./kong-config.js"
|
|
6
7
|
|
|
7
8
|
export interface SelfHostComposePaths {
|
|
@@ -33,43 +34,138 @@ export function staticDirForCompose(config: SupatypeProjectConfig): string | und
|
|
|
33
34
|
return dir && dir.length > 0 ? dir : "./public"
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Bind-mount source for `/project` in generated compose files.
|
|
39
|
+
* Paths are resolved from `--project-directory` (always the project root in `runDockerCompose`),
|
|
40
|
+
* not from the compose file directory — use `.` not `../..`.
|
|
41
|
+
*/
|
|
42
|
+
function projectMountPath(_cwd: string): string {
|
|
43
|
+
return "."
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Paths in generated compose are resolved from `--project-directory` (project root). */
|
|
47
|
+
function relativeFromProjectRoot(cwd: string, target: string): string {
|
|
48
|
+
let rel = relative(resolve(cwd), resolve(target)).replace(/\\/g, "/")
|
|
39
49
|
if (!rel.startsWith(".") && !rel.startsWith("/")) {
|
|
40
50
|
rel = `./${rel}`
|
|
41
51
|
}
|
|
42
|
-
return rel
|
|
52
|
+
return rel
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function kongMountPath(_cwd: string): string {
|
|
56
|
+
return ".supatype/self-host/kong.yml"
|
|
43
57
|
}
|
|
44
58
|
|
|
45
|
-
|
|
59
|
+
/** Host Vite dev server as seen from Kong inside Docker Compose. */
|
|
60
|
+
export const COMPOSE_STUDIO_HOST_URL = "http://host.docker.internal:3002"
|
|
61
|
+
|
|
62
|
+
/** Local monorepo Studio image for `supatype dev` (dogfooding). */
|
|
63
|
+
function studioServiceBlock(cwd: string, devLocal: boolean): string {
|
|
64
|
+
if (!devLocal) {
|
|
65
|
+
return ` image: \${SUPATYPE_STUDIO_IMAGE:-supatype/studio:latest}`
|
|
66
|
+
}
|
|
67
|
+
const monorepoRoot = resolve(cwd, "..", "supatype")
|
|
68
|
+
const studioDockerfile = join(monorepoRoot, "packages", "studio", "Dockerfile")
|
|
69
|
+
if (!existsSync(studioDockerfile) || !existsSync(join(monorepoRoot, "pnpm-workspace.yaml"))) {
|
|
70
|
+
return ` image: \${SUPATYPE_STUDIO_IMAGE:-supatype/studio:latest}`
|
|
71
|
+
}
|
|
72
|
+
const context = relativeFromProjectRoot(cwd, monorepoRoot)
|
|
73
|
+
return ` build:
|
|
74
|
+
context: ${context}
|
|
75
|
+
dockerfile: packages/studio/Dockerfile
|
|
76
|
+
image: supatype/studio:dev-local`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Host dev app (Astro/Vite on the machine) as seen from inside compose services. */
|
|
80
|
+
function proxyUpstreamForCompose(upstream: string, devLocal: boolean): string {
|
|
81
|
+
const trimmed = upstream.trim()
|
|
82
|
+
if (!devLocal) return trimmed
|
|
83
|
+
try {
|
|
84
|
+
const url = new URL(trimmed)
|
|
85
|
+
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
|
86
|
+
url.hostname = "host.docker.internal"
|
|
87
|
+
return url.toString()
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// keep literal upstream when not a URL
|
|
91
|
+
}
|
|
92
|
+
return trimmed
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function serverAppEnvForCompose(config: SupatypeProjectConfig, devLocal: boolean): string {
|
|
46
96
|
const mode = config.app.mode ?? "none"
|
|
47
97
|
const lines = [` SUPATYPE_APP_MODE: ${mode}`]
|
|
48
98
|
if (mode === "static") {
|
|
49
99
|
const dir = staticDirForCompose(config) ?? "./public"
|
|
50
100
|
lines.push(` SUPATYPE_APP_STATIC_DIR: /project/${dir.replace(/^\.\//, "")}`)
|
|
51
101
|
} else if (mode === "proxy" && config.app.upstream?.trim()) {
|
|
52
|
-
lines.push(` SUPATYPE_APP_UPSTREAM: ${config.app.upstream
|
|
102
|
+
lines.push(` SUPATYPE_APP_UPSTREAM: ${proxyUpstreamForCompose(config.app.upstream, devLocal)}`)
|
|
53
103
|
}
|
|
54
104
|
return lines.join("\n")
|
|
55
105
|
}
|
|
56
106
|
|
|
57
|
-
export
|
|
107
|
+
export interface SelfHostComposeOptions {
|
|
108
|
+
/** `supatype dev` with provider docker: internal-only db/server; Kong on host :18473. */
|
|
109
|
+
devLocal?: boolean
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function renderSelfHostCompose(
|
|
113
|
+
config: SupatypeProjectConfig,
|
|
114
|
+
cwd: string = process.cwd(),
|
|
115
|
+
options?: SelfHostComposeOptions,
|
|
116
|
+
): string {
|
|
58
117
|
const projectMount = projectMountPath(cwd)
|
|
59
|
-
const
|
|
118
|
+
const kongMount = kongMountPath(cwd)
|
|
119
|
+
const devLocal = options?.devLocal === true
|
|
120
|
+
const studioHostDev = devLocal && hasStudioOverride(config)
|
|
121
|
+
const appEnv = serverAppEnvForCompose(config, devLocal)
|
|
122
|
+
const studioService = studioServiceBlock(cwd, devLocal)
|
|
123
|
+
const studioBlock = studioHostDev
|
|
124
|
+
? ""
|
|
125
|
+
: `
|
|
126
|
+
studio:
|
|
127
|
+
${studioService}
|
|
128
|
+
environment:
|
|
129
|
+
SUPATYPE_CLOUD_JSON: '{"url":"\${API_EXTERNAL_URL:-http://localhost:18473}","anonKey":"\${ANON_KEY:-}"}'
|
|
130
|
+
expose:
|
|
131
|
+
- "3002"
|
|
132
|
+
`
|
|
133
|
+
const kongDependsOn = studioHostDev
|
|
134
|
+
? ` - server`
|
|
135
|
+
: ` - server
|
|
136
|
+
- studio`
|
|
137
|
+
const publishDbToHost = !devLocal || hasEngineOverride(config)
|
|
138
|
+
const dbPorts = publishDbToHost
|
|
139
|
+
? devLocal
|
|
140
|
+
? ` ports:
|
|
141
|
+
- "127.0.0.1:\${SUPATYPE_DEV_DB_PORT:-54329}:5432"
|
|
142
|
+
`
|
|
143
|
+
: ` ports:
|
|
144
|
+
- "5432:5432"
|
|
145
|
+
`
|
|
146
|
+
: ""
|
|
147
|
+
const serverPorts = devLocal
|
|
148
|
+
? ""
|
|
149
|
+
: ` ports:
|
|
150
|
+
- "9999:9999"
|
|
151
|
+
`
|
|
152
|
+
const minioPorts = devLocal
|
|
153
|
+
? ""
|
|
154
|
+
: ` ports:
|
|
155
|
+
- "9000:9000"
|
|
156
|
+
- "9001:9001"
|
|
157
|
+
`
|
|
60
158
|
|
|
61
159
|
return `# Generated by supatype self-host compose
|
|
62
160
|
# Kong → supatype-server (unified gateway) → internal PostgREST / storage / etc.
|
|
63
161
|
services:
|
|
64
162
|
db:
|
|
65
|
-
image: \${SUPATYPE_POSTGRES_IMAGE:-supatype/postgres:
|
|
163
|
+
image: \${SUPATYPE_POSTGRES_IMAGE:-supatype/postgres:latest}
|
|
66
164
|
environment:
|
|
67
165
|
POSTGRES_USER: \${POSTGRES_USER:-supatype_admin}
|
|
68
166
|
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
|
|
69
167
|
POSTGRES_DB: \${POSTGRES_DB:-supatype}
|
|
70
|
-
|
|
71
|
-
- "5432:5432"
|
|
72
|
-
volumes:
|
|
168
|
+
${dbPorts} volumes:
|
|
73
169
|
- db-data:/var/lib/postgresql/data
|
|
74
170
|
healthcheck:
|
|
75
171
|
test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-supatype_admin}"]
|
|
@@ -83,7 +179,7 @@ services:
|
|
|
83
179
|
- "3000"
|
|
84
180
|
environment:
|
|
85
181
|
PGRST_DB_URI: postgresql://\${POSTGRES_USER:-supatype_admin}:\${POSTGRES_PASSWORD:-postgres}@db:5432/\${POSTGRES_DB:-supatype}
|
|
86
|
-
PGRST_DB_SCHEMA: "public, supatype"
|
|
182
|
+
PGRST_DB_SCHEMA: "public, supatype, graphql_public"
|
|
87
183
|
PGRST_DB_ANON_ROLE: anon
|
|
88
184
|
PGRST_JWT_SECRET: \${JWT_SECRET:-super-secret-jwt-token-change-in-production}
|
|
89
185
|
PGRST_DB_EXTRA_SEARCH_PATH: public,extensions
|
|
@@ -122,19 +218,20 @@ services:
|
|
|
122
218
|
SUPATYPE_URL: \${API_EXTERNAL_URL:-http://localhost:18473}
|
|
123
219
|
SUPATYPE_ANON_KEY: \${ANON_KEY:-}
|
|
124
220
|
SUPATYPE_SERVICE_ROLE_KEY: \${SERVICE_ROLE_KEY:-}
|
|
221
|
+
STRIPE_SECRET_KEY: \${STRIPE_SECRET_KEY:-}
|
|
222
|
+
STRIPE_WEBHOOK_SECRET: \${STRIPE_WEBHOOK_SECRET:-}
|
|
223
|
+
SITE_URL: \${SITE_URL:-\${API_EXTERNAL_URL:-http://localhost:18473}}
|
|
125
224
|
depends_on:
|
|
126
225
|
db:
|
|
127
226
|
condition: service_healthy
|
|
128
227
|
|
|
129
228
|
server:
|
|
130
229
|
image: \${SUPATYPE_SERVER_IMAGE:-\${SUPATYPE_AUTH_IMAGE:-supatype/server:latest}}
|
|
131
|
-
|
|
132
|
-
- "9999:9999"
|
|
133
|
-
volumes:
|
|
230
|
+
${serverPorts} volumes:
|
|
134
231
|
- ${projectMount}:/project:ro
|
|
135
232
|
working_dir: /project
|
|
136
233
|
environment:
|
|
137
|
-
SUPATYPE_MODE: standalone
|
|
234
|
+
SUPATYPE_MODE: ${devLocal ? "dev" : "standalone"}
|
|
138
235
|
SUPATYPE_MANIFEST_PATH: /project/.supatype/manifest.json
|
|
139
236
|
SUPATYPE_ADMIN_CONFIG_PATH: /project/.supatype/admin-config.json
|
|
140
237
|
SUPATYPE_API_CONFIG_PATH: /project/.supatype/api-config.json
|
|
@@ -162,6 +259,7 @@ ${appEnv}
|
|
|
162
259
|
GOTRUE_JWT_ADMIN_ROLES: service_role,supatype_admin
|
|
163
260
|
GOTRUE_MAILER_AUTOCONFIRM: \${GOTRUE_MAILER_AUTOCONFIRM:-true}
|
|
164
261
|
GOTRUE_DISABLE_SIGNUP: \${DISABLE_SIGNUP:-false}
|
|
262
|
+
${devLocal ? " STUDIO_OPEN_DEV: \"1\"\n" : ""}
|
|
165
263
|
depends_on:
|
|
166
264
|
db:
|
|
167
265
|
condition: service_healthy
|
|
@@ -178,17 +276,20 @@ ${appEnv}
|
|
|
178
276
|
environment:
|
|
179
277
|
MINIO_ROOT_USER: supatype
|
|
180
278
|
MINIO_ROOT_PASSWORD: supatype-secret
|
|
181
|
-
|
|
182
|
-
- "9000:9000"
|
|
183
|
-
- "9001:9001"
|
|
184
|
-
volumes:
|
|
279
|
+
${minioPorts} volumes:
|
|
185
280
|
- minio-data:/data
|
|
186
281
|
|
|
187
|
-
|
|
188
|
-
image: \${
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
282
|
+
schema-engine:
|
|
283
|
+
image: \${SUPATYPE_ENGINE_IMAGE:-supatype/schema-engine:latest}
|
|
284
|
+
profiles: ["tools"]
|
|
285
|
+
entrypoint: ["supatype-engine"]
|
|
286
|
+
volumes:
|
|
287
|
+
- ${projectMount}:/project
|
|
288
|
+
working_dir: /project
|
|
289
|
+
depends_on:
|
|
290
|
+
db:
|
|
291
|
+
condition: service_healthy
|
|
292
|
+
${studioBlock}
|
|
192
293
|
kong:
|
|
193
294
|
image: kong:3.6
|
|
194
295
|
environment:
|
|
@@ -199,12 +300,11 @@ ${appEnv}
|
|
|
199
300
|
KONG_PROXY_ERROR_LOG: /dev/stderr
|
|
200
301
|
KONG_ADMIN_ERROR_LOG: /dev/stderr
|
|
201
302
|
volumes:
|
|
202
|
-
-
|
|
303
|
+
- ${kongMount}:/etc/kong/kong.yml:ro
|
|
203
304
|
ports:
|
|
204
|
-
- "18473:8000"
|
|
305
|
+
- "\${SUPATYPE_KONG_PORT:-18473}:8000"
|
|
205
306
|
depends_on:
|
|
206
|
-
|
|
207
|
-
- studio
|
|
307
|
+
${kongDependsOn}
|
|
208
308
|
|
|
209
309
|
volumes:
|
|
210
310
|
db-data:
|
|
@@ -226,24 +326,50 @@ function ensureComposeManifest(cwd: string): void {
|
|
|
226
326
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8")
|
|
227
327
|
}
|
|
228
328
|
|
|
229
|
-
|
|
329
|
+
function ensureProjectFunctionsDir(cwd: string, config: SupatypeProjectConfig): void {
|
|
330
|
+
mkdirSync(preferredFunctionsPathFromProject(config, cwd), { recursive: true })
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function writeSelfHostCompose(
|
|
334
|
+
cwd: string,
|
|
335
|
+
config: SupatypeProjectConfig,
|
|
336
|
+
options?: SelfHostComposeOptions,
|
|
337
|
+
): SelfHostComposePaths {
|
|
230
338
|
const paths = selfHostComposePaths(cwd)
|
|
231
339
|
mkdirSync(paths.dir, { recursive: true })
|
|
340
|
+
ensureProjectFunctionsDir(cwd, config)
|
|
232
341
|
ensureComposeManifest(cwd)
|
|
233
|
-
writeFileSync(paths.composePath, renderSelfHostCompose(config, cwd), "utf8")
|
|
342
|
+
writeFileSync(paths.composePath, renderSelfHostCompose(config, cwd, options), "utf8")
|
|
343
|
+
const studioHostDev = options?.devLocal === true && hasStudioOverride(config)
|
|
234
344
|
writeFileSync(
|
|
235
345
|
paths.kongPath,
|
|
236
346
|
buildKongDeclarative({
|
|
237
347
|
unifiedGateway: true,
|
|
348
|
+
...(studioHostDev && {
|
|
349
|
+
studioServiceUrl: COMPOSE_STUDIO_HOST_URL,
|
|
350
|
+
studioStripPath: false,
|
|
351
|
+
}),
|
|
238
352
|
}),
|
|
239
353
|
"utf8",
|
|
240
354
|
)
|
|
241
355
|
return paths
|
|
242
356
|
}
|
|
243
357
|
|
|
244
|
-
export function runDockerCompose(
|
|
358
|
+
export function runDockerCompose(
|
|
359
|
+
composePath: string,
|
|
360
|
+
args: string[],
|
|
361
|
+
projectRoot: string = process.cwd(),
|
|
362
|
+
composeProject?: string,
|
|
363
|
+
): number {
|
|
245
364
|
const envFile = resolve(projectRoot, ".env")
|
|
246
|
-
const composeArgs = ["compose"
|
|
365
|
+
const composeArgs = ["compose"]
|
|
366
|
+
// Per-project name isolates containers/volumes/network so multiple Supatype
|
|
367
|
+
// projects on one machine never share a database (default would be the
|
|
368
|
+
// ".supatype/self-host" dir name, identical for every project).
|
|
369
|
+
if (composeProject) composeArgs.push("-p", composeProject)
|
|
370
|
+
// Resolve ${VAR} in compose.yml from the project root .env (not .supatype/self-host/).
|
|
371
|
+
composeArgs.push("--project-directory", projectRoot)
|
|
372
|
+
composeArgs.push("-f", composePath)
|
|
247
373
|
if (existsSync(envFile)) {
|
|
248
374
|
composeArgs.push("--env-file", envFile)
|
|
249
375
|
}
|
|
@@ -251,7 +377,13 @@ export function runDockerCompose(composePath: string, args: string[], projectRoo
|
|
|
251
377
|
const result = spawnSync(
|
|
252
378
|
"docker",
|
|
253
379
|
composeArgs,
|
|
254
|
-
{ stdio: "inherit", cwd:
|
|
380
|
+
{ stdio: "inherit", cwd: projectRoot },
|
|
255
381
|
)
|
|
256
382
|
return result.status ?? 1
|
|
257
383
|
}
|
|
384
|
+
|
|
385
|
+
/** Compose project name for a Supatype project — isolates docker state per project. */
|
|
386
|
+
export function composeProjectName(projectName: string): string {
|
|
387
|
+
const slug = projectName.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "")
|
|
388
|
+
return `supatype-${slug || "project"}`
|
|
389
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SupatypeProjectConfig } from "./project-config.js"
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_STUDIO_ADMIN_ROLES = ["admin", "supatype_admin"] as const
|
|
4
|
+
|
|
5
|
+
/** Studio admin roles from `supatype.config.ts` `admin.roles` or defaults. */
|
|
6
|
+
export function studioAdminRoles(cfg: SupatypeProjectConfig): string[] {
|
|
7
|
+
const roles = cfg.admin?.roles
|
|
8
|
+
if (roles !== undefined && roles.length > 0) return roles
|
|
9
|
+
return [...DEFAULT_STUDIO_ADMIN_ROLES]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Merge `adminRoles` into engine admin-config JSON for Studio and supatype-server. */
|
|
13
|
+
export function withAdminRoles(admin: unknown, cfg: SupatypeProjectConfig): Record<string, unknown> {
|
|
14
|
+
const base = typeof admin === "object" && admin !== null ? (admin as Record<string, unknown>) : {}
|
|
15
|
+
return { ...base, adminRoles: studioAdminRoles(cfg) }
|
|
16
|
+
}
|