@supatype/cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +190 -153
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/api-config-cache.d.ts +34 -0
- package/dist/api-config-cache.d.ts.map +1 -0
- package/dist/api-config-cache.js +123 -0
- package/dist/api-config-cache.js.map +1 -0
- package/dist/cache-identity-scope.d.ts +38 -0
- package/dist/cache-identity-scope.d.ts.map +1 -0
- package/dist/cache-identity-scope.js +70 -0
- package/dist/cache-identity-scope.js.map +1 -0
- package/dist/cache-provider.d.ts +30 -0
- package/dist/cache-provider.d.ts.map +1 -0
- package/dist/cache-provider.js +60 -0
- package/dist/cache-provider.js.map +1 -0
- package/dist/cli-version-embedded.js +1 -1
- package/dist/commands/add.js +1 -1
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/cache.d.ts +1 -1
- package/dist/commands/cache.js +3 -3
- package/dist/commands/cache.js.map +1 -1
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +2 -1
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +2 -1
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +61 -8
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +2 -1
- package/dist/commands/diff.js.map +1 -1
- package/dist/commands/functions.d.ts.map +1 -1
- package/dist/commands/functions.js +6 -2
- package/dist/commands/functions.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +13 -9
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/keys.d.ts +14 -0
- package/dist/commands/keys.d.ts.map +1 -1
- package/dist/commands/keys.js +37 -0
- package/dist/commands/keys.js.map +1 -1
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +2 -1
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +52 -1
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/self-host.d.ts.map +1 -1
- package/dist/commands/self-host.js +9 -0
- package/dist/commands/self-host.js.map +1 -1
- package/dist/compose-services.d.ts.map +1 -1
- package/dist/compose-services.js +2 -1
- package/dist/compose-services.js.map +1 -1
- package/dist/dev-compose.d.ts.map +1 -1
- package/dist/dev-compose.js +18 -7
- package/dist/dev-compose.js.map +1 -1
- package/dist/engine-floor.d.ts +11 -1
- package/dist/engine-floor.d.ts.map +1 -1
- package/dist/engine-floor.js +49 -16
- package/dist/engine-floor.js.map +1 -1
- package/dist/functions-deno-types.d.ts +9 -0
- package/dist/functions-deno-types.d.ts.map +1 -1
- package/dist/functions-deno-types.js +45 -1
- package/dist/functions-deno-types.js.map +1 -1
- package/dist/functions-router-gen.d.ts.map +1 -1
- package/dist/functions-router-gen.js +113 -81
- package/dist/functions-router-gen.js.map +1 -1
- package/dist/kong-config.d.ts +1 -1
- package/dist/model-cache.d.ts +17 -0
- package/dist/model-cache.d.ts.map +1 -0
- package/dist/model-cache.js +45 -0
- package/dist/model-cache.js.map +1 -0
- package/dist/model-hooks.d.ts +5 -0
- package/dist/model-hooks.d.ts.map +1 -1
- package/dist/model-hooks.js +29 -1
- package/dist/model-hooks.js.map +1 -1
- package/dist/model-versioning.d.ts +80 -0
- package/dist/model-versioning.d.ts.map +1 -0
- package/dist/model-versioning.js +140 -0
- package/dist/model-versioning.js.map +1 -0
- package/dist/postgres-ctl.d.ts +55 -0
- package/dist/postgres-ctl.d.ts.map +1 -1
- package/dist/postgres-ctl.js +84 -8
- package/dist/postgres-ctl.js.map +1 -1
- package/dist/preview-config-check.d.ts +21 -0
- package/dist/preview-config-check.d.ts.map +1 -0
- package/dist/preview-config-check.js +47 -0
- package/dist/preview-config-check.js.map +1 -0
- package/dist/project-config.d.ts +182 -5
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +125 -8
- package/dist/project-config.js.map +1 -1
- package/dist/resolve-target.d.ts +9 -0
- package/dist/resolve-target.d.ts.map +1 -1
- package/dist/resolve-target.js.map +1 -1
- package/dist/rest-cache-admin.d.ts.map +1 -1
- package/dist/rest-cache-admin.js +6 -2
- package/dist/rest-cache-admin.js.map +1 -1
- package/dist/schema-ast-v2.d.ts +32 -1
- package/dist/schema-ast-v2.d.ts.map +1 -1
- package/dist/schema-ast-v2.js +14 -1
- package/dist/schema-ast-v2.js.map +1 -1
- package/dist/self-host-compose.d.ts +20 -1
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +181 -27
- package/dist/self-host-compose.js.map +1 -1
- package/dist/studio-admin-roles.d.ts +8 -1
- package/dist/studio-admin-roles.d.ts.map +1 -1
- package/dist/studio-admin-roles.js +16 -2
- package/dist/studio-admin-roles.js.map +1 -1
- package/dist/type-extractor.d.ts +7 -0
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +234 -3
- package/dist/type-extractor.js.map +1 -1
- package/package.json +2 -2
- package/src/api-config-cache.ts +142 -0
- package/src/cache-identity-scope.ts +92 -0
- package/src/cache-provider.ts +61 -0
- package/src/cli-version-embedded.ts +1 -1
- package/src/commands/add.ts +1 -1
- package/src/commands/cache.ts +3 -3
- package/src/commands/cloud.ts +2 -1
- package/src/commands/deploy.ts +2 -1
- package/src/commands/dev.ts +68 -7
- package/src/commands/diff.ts +2 -1
- package/src/commands/functions.ts +6 -2
- package/src/commands/init.ts +13 -9
- package/src/commands/keys.ts +40 -0
- package/src/commands/migrate.ts +2 -1
- package/src/commands/push.ts +57 -1
- package/src/commands/self-host.ts +9 -0
- package/src/compose-services.ts +2 -1
- package/src/dev-compose.ts +18 -7
- package/src/engine-floor.ts +73 -16
- package/src/functions-deno-types.ts +48 -1
- package/src/functions-router-gen.ts +113 -81
- package/src/kong-config.ts +1 -1
- package/src/model-cache.ts +74 -0
- package/src/model-hooks.ts +26 -2
- package/src/model-versioning.ts +167 -0
- package/src/postgres-ctl.ts +103 -9
- package/src/preview-config-check.ts +52 -0
- package/src/project-config.ts +273 -11
- package/src/resolve-target.ts +11 -1
- package/src/rest-cache-admin.ts +8 -2
- package/src/schema-ast-v2.ts +47 -0
- package/src/self-host-compose.ts +204 -27
- package/src/studio-admin-roles.ts +16 -2
- package/src/type-extractor.ts +281 -2
- package/tests/api-config-cache-seed.test.ts +180 -0
- package/tests/cache-identity-scope.test.ts +61 -0
- package/tests/cache-provider-config.test.ts +132 -0
- package/tests/engine-floor.test.ts +43 -0
- package/tests/external-database-compose.test.ts +74 -3
- package/tests/field-masking-tier.test.ts +3 -3
- package/tests/fixtures/identity-scope-corpus.json +42 -0
- package/tests/functions-concurrency.test.ts +71 -0
- package/tests/init.test.ts +36 -2
- package/tests/model-cache-declaration.test.ts +229 -0
- package/tests/model-cache-manifest.test.ts +99 -0
- package/tests/model-hooks.test.ts +55 -0
- package/tests/model-versions.test.ts +229 -0
- package/tests/native-keyspace.test.ts +130 -0
- package/tests/preview-config-check.test.ts +104 -0
- package/tests/rest-cache-admin.test.ts +36 -0
- package/tests/runtime-contract.test.ts +176 -22
- package/tests/self-host-keys.test.ts +92 -0
- package/tests/studio-admin-roles.test.ts +32 -0
- package/tests/type-extractor.test.ts +113 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/project-config.ts
CHANGED
|
@@ -100,6 +100,44 @@ export interface SupatypeProjectConfig {
|
|
|
100
100
|
provider?: "kong" | "none"
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Where the REST response cache and Kong's ACME certificates live.
|
|
105
|
+
*
|
|
106
|
+
* Both speak RESP, so this is a choice of server rather than of feature.
|
|
107
|
+
*/
|
|
108
|
+
cache?: {
|
|
109
|
+
/**
|
|
110
|
+
* "pg_keyspace" (default) = the RESP keyspace inside the Postgres
|
|
111
|
+
* container, so the stack runs one stateful
|
|
112
|
+
* service instead of two.
|
|
113
|
+
* "valkey" = a Valkey sidecar container.
|
|
114
|
+
*
|
|
115
|
+
* pg_keyspace needs `shared_preload_libraries`, which is a property of the
|
|
116
|
+
* Postgres this stack starts — so it is only available when Supatype
|
|
117
|
+
* provisions the database. With `database.external` the default resolves
|
|
118
|
+
* to Valkey, because there is no container to configure; asking for
|
|
119
|
+
* pg_keyspace explicitly there is rejected rather than ignored.
|
|
120
|
+
*
|
|
121
|
+
* Set "valkey" to keep the sidecar. Nothing about the cache's behaviour
|
|
122
|
+
* changes with the answer — both speak RESP and both hold the same keys.
|
|
123
|
+
*/
|
|
124
|
+
provider?: "valkey" | "pg_keyspace"
|
|
125
|
+
/**
|
|
126
|
+
* Extra key prefixes to persist when `provider` is "pg_keyspace".
|
|
127
|
+
*
|
|
128
|
+
* The keyspace is a cache: everything is ephemeral, living in shared
|
|
129
|
+
* memory and not surviving a restart of the database. The exception is
|
|
130
|
+
* Kong's ACME certificates, which are always kept — you do not have to
|
|
131
|
+
* name them, and re-issuing them on every restart would meet Let's
|
|
132
|
+
* Encrypt's rate limits.
|
|
133
|
+
*
|
|
134
|
+
* List a prefix here only for something you are storing yourself that
|
|
135
|
+
* must outlive a restart. Each durable write goes through the WAL, which
|
|
136
|
+
* is the cost being avoided everywhere else. Each entry is a prefix of the
|
|
137
|
+
* key as stored — see `pg_keyspace.durability_overrides`.
|
|
138
|
+
*/
|
|
139
|
+
durablePrefixes?: string[]
|
|
140
|
+
}
|
|
103
141
|
app: {
|
|
104
142
|
/**
|
|
105
143
|
* How the root path "/" is handled by supatype-server.
|
|
@@ -225,6 +263,54 @@ export interface SupatypeProjectConfig {
|
|
|
225
263
|
*/
|
|
226
264
|
api_schemas?: readonly string[]
|
|
227
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Drafts and preview links, for the models that declare `versions`.
|
|
268
|
+
*
|
|
269
|
+
* Project-wide rather than per model: a draft is a draft whichever model it belongs to, and a
|
|
270
|
+
* reviewer granted sight of one is not granted it a table at a time.
|
|
271
|
+
*/
|
|
272
|
+
publishing?: {
|
|
273
|
+
/**
|
|
274
|
+
* Studio roles that see every unpublished draft and its history, beyond the record's own
|
|
275
|
+
* creator. Defaults to all three, `["admin", "developer", "editor"]`.
|
|
276
|
+
*
|
|
277
|
+
* The record's creator always sees their own drafts and is not listed here. Narrowing this is
|
|
278
|
+
* the interesting case, and it has a trap: whoever may **update** a record may draft and publish
|
|
279
|
+
* it, so a role that can edit but cannot see drafts will build its save on the live row and
|
|
280
|
+
* discard a colleague's pending work. Take a role off this list only where it also cannot edit.
|
|
281
|
+
*
|
|
282
|
+
* An empty list narrows visibility to creators alone.
|
|
283
|
+
*
|
|
284
|
+
* `anon` is never on this list and cannot be put on it. A draft schema readable without
|
|
285
|
+
* credentials would make publishing mean nothing.
|
|
286
|
+
*
|
|
287
|
+
* Studio can override this at runtime for a project that needs to change it without a push.
|
|
288
|
+
* This is the reviewable default that lives in git.
|
|
289
|
+
*/
|
|
290
|
+
draft_visibility?: readonly ("admin" | "developer" | "editor")[]
|
|
291
|
+
/** Signed, expiring links that let someone without an account read a draft. */
|
|
292
|
+
preview?: {
|
|
293
|
+
/** Lifetime of a minted link when the request does not ask for one, in seconds. Default 900. */
|
|
294
|
+
default_ttl?: number
|
|
295
|
+
/** Ceiling for a link covering one record, in seconds. Default 604800, seven days. */
|
|
296
|
+
max_record_ttl?: number
|
|
297
|
+
/**
|
|
298
|
+
* Ceiling for a link covering every draft in the project, in seconds. Default 86400, one day.
|
|
299
|
+
*
|
|
300
|
+
* Shorter than a record link on purpose: the blast radius is every unpublished draft there is,
|
|
301
|
+
* so the window in which a leaked link is still worth anything should be smaller.
|
|
302
|
+
*/
|
|
303
|
+
max_project_ttl?: number
|
|
304
|
+
/**
|
|
305
|
+
* Whether project-scoped links may be minted at all. Default true.
|
|
306
|
+
*
|
|
307
|
+
* Set false for a project where nothing should ever be shareable in bulk; record links keep
|
|
308
|
+
* working. Minting one is already restricted to `admin` and `developer`, since nobody can
|
|
309
|
+
* grant sight of drafts they cannot see themselves.
|
|
310
|
+
*/
|
|
311
|
+
allow_project_scope?: boolean
|
|
312
|
+
}
|
|
313
|
+
}
|
|
228
314
|
functions?: {
|
|
229
315
|
/** Path to edge functions directory, relative to `supatype.root` when not absolute. */
|
|
230
316
|
path?: string
|
|
@@ -286,6 +372,37 @@ export interface SupatypeProjectConfig {
|
|
|
286
372
|
admin?: {
|
|
287
373
|
/** JWT `app_metadata.role` values allowed to use Studio. Default: admin, supatype_admin */
|
|
288
374
|
roles?: string[]
|
|
375
|
+
/**
|
|
376
|
+
* Where each model renders in your own front end, keyed by model name.
|
|
377
|
+
*
|
|
378
|
+
* Studio needs this for two things that look alike and are not: the live preview pane, which
|
|
379
|
+
* streams unsaved keystrokes into an iframe, and a shared preview link, which points somebody
|
|
380
|
+
* with no account at a saved draft. Both need the same answer to "what is the address of this
|
|
381
|
+
* record", and neither can guess it.
|
|
382
|
+
*
|
|
383
|
+
* Without it there is no share link to give out. Studio says so rather than handing over the
|
|
384
|
+
* bare code, because a credential with nowhere to open it reads as a bug.
|
|
385
|
+
*
|
|
386
|
+
* `urlPattern` names fields in braces and they are filled from the record in hand, so a post
|
|
387
|
+
* previews at the address its current slug implies.
|
|
388
|
+
*
|
|
389
|
+
* **A path is enough when this deployment serves the app.** With `app.mode` set to `static` or
|
|
390
|
+
* `proxy`, the app is served at `/` on the same origin as the API, and Studio resolves a path
|
|
391
|
+
* against the origin it is loaded from. Writing the origin out again only risks it going stale:
|
|
392
|
+
*
|
|
393
|
+
* ```ts
|
|
394
|
+
* admin: { livePreview: { Post: { urlPattern: "/preview/{slug}" } } }
|
|
395
|
+
* ```
|
|
396
|
+
*
|
|
397
|
+
* With `app.mode: "none"` the app is somewhere else and no deployment fact can say where, so
|
|
398
|
+
* give an absolute URL. `supatype push` refuses a path-only pattern in that mode rather than
|
|
399
|
+
* letting it become a link that resolves to nothing.
|
|
400
|
+
*
|
|
401
|
+
* ```ts
|
|
402
|
+
* admin: { livePreview: { Post: { urlPattern: "https://example.com/preview/{slug}" } } }
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
405
|
+
livePreview?: Record<string, { url?: string; urlPattern?: string }>
|
|
289
406
|
}
|
|
290
407
|
}
|
|
291
408
|
|
|
@@ -407,10 +524,54 @@ export function validateProjectConfig(raw: unknown, filename: string): SupatypeP
|
|
|
407
524
|
}
|
|
408
525
|
|
|
409
526
|
validateExternalDatabase(cfg, filename)
|
|
527
|
+
validateCache(cfg, filename)
|
|
410
528
|
|
|
411
529
|
return raw as SupatypeProjectConfig
|
|
412
530
|
}
|
|
413
531
|
|
|
532
|
+
/**
|
|
533
|
+
* `cache.provider = "pg_keyspace"` describes how the Postgres this stack
|
|
534
|
+
* starts is configured, so it cannot be honoured against a database Supatype
|
|
535
|
+
* does not manage. Rejected rather than silently falling back to Valkey: a
|
|
536
|
+
* stack that quietly runs the thing you switched away from is worse than one
|
|
537
|
+
* that will not start.
|
|
538
|
+
*/
|
|
539
|
+
function validateCache(cfg: Record<string, unknown>, filename: string): void {
|
|
540
|
+
const cache = cfg["cache"] as Record<string, unknown> | undefined
|
|
541
|
+
if (!cache) return
|
|
542
|
+
const provider = cache["provider"]
|
|
543
|
+
if (provider !== undefined && provider !== "valkey" && provider !== "pg_keyspace") {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`${filename}: cache.provider must be "valkey" or "pg_keyspace" (got ${JSON.stringify(provider)})`,
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
const database = cfg["database"] as Record<string, unknown> | undefined
|
|
549
|
+
if (provider === "pg_keyspace" && database?.["external"]) {
|
|
550
|
+
throw new Error(
|
|
551
|
+
`${filename}: cache.provider = "pg_keyspace" needs the Postgres this stack starts — ` +
|
|
552
|
+
`it is loaded with shared_preload_libraries, which is not something Supatype can set on ` +
|
|
553
|
+
`a database.external one. Use cache.provider = "valkey", or drop database.external.`,
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
const prefixes = cache["durablePrefixes"]
|
|
557
|
+
if (prefixes !== undefined) {
|
|
558
|
+
if (!Array.isArray(prefixes) || prefixes.some((p) => typeof p !== "string" || p.length === 0)) {
|
|
559
|
+
throw new Error(`${filename}: cache.durablePrefixes must be an array of non-empty strings`)
|
|
560
|
+
}
|
|
561
|
+
// `,` separates entries and `=` separates a prefix from its tier in
|
|
562
|
+
// pg_keyspace.durability_overrides, so a prefix containing either would be
|
|
563
|
+
// read as two settings. Refused here rather than mangled into the command
|
|
564
|
+
// line, where it would surface as a Postgres that will not start.
|
|
565
|
+
const bad = (prefixes as string[]).find((p) => p.includes(",") || p.includes("="))
|
|
566
|
+
if (bad) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
`${filename}: cache.durablePrefixes entry ${JSON.stringify(bad)} cannot contain "," or "=" — ` +
|
|
569
|
+
`both are separators in pg_keyspace.durability_overrides`,
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
414
575
|
/**
|
|
415
576
|
* Rules for `database.external`, all of them errors rather than precedence.
|
|
416
577
|
*
|
|
@@ -635,6 +796,22 @@ export function pgSchema(cfg: SupatypeProjectConfig): string {
|
|
|
635
796
|
*/
|
|
636
797
|
export const STACK_API_SCHEMAS = ["supatype", "graphql_public", "auth"] as const
|
|
637
798
|
|
|
799
|
+
/**
|
|
800
|
+
* The schema holding generated draft views, one per versioned model.
|
|
801
|
+
*
|
|
802
|
+
* Its own schema rather than a suffix on the table name, so a draft has the same table name and the
|
|
803
|
+
* same row type as what it is a draft of, and a client reaches it by switching profile.
|
|
804
|
+
*/
|
|
805
|
+
export const DRAFT_SCHEMA = "draft"
|
|
806
|
+
|
|
807
|
+
/** What shapes the exposed-schema list, beyond the config itself. */
|
|
808
|
+
export type ApiSchemaOptions = {
|
|
809
|
+
/** How field rules will be enforced here. `views` moves the managed schema off the list. */
|
|
810
|
+
tier?: "none" | "extension" | "views" | undefined
|
|
811
|
+
/** True when any model declares `versions`, which is what puts the `draft` schema on the list. */
|
|
812
|
+
drafts?: boolean | undefined
|
|
813
|
+
}
|
|
814
|
+
|
|
638
815
|
/**
|
|
639
816
|
* Schemas to expose over REST, as `PGRST_DB_SCHEMA` wants them.
|
|
640
817
|
*
|
|
@@ -644,18 +821,24 @@ export const STACK_API_SCHEMAS = ["supatype", "graphql_public", "auth"] as const
|
|
|
644
821
|
* output mentioned the setting that caused it.
|
|
645
822
|
*
|
|
646
823
|
* `api_schemas` replaces the whole list when stated, including the stack schemas, so dropping
|
|
647
|
-
* `supatype` from it is a supported way to stop exposing Studio's views.
|
|
648
|
-
*
|
|
649
|
-
*
|
|
824
|
+
* `supatype` from it is a supported way to stop exposing Studio's views. That holds for `draft` too:
|
|
825
|
+
* an explicit list has to name it, and `db check` reports a project whose models declare `versions`
|
|
826
|
+
* against a list that does not. Order is preserved and duplicates removed: PostgREST serves the first
|
|
827
|
+
* entry as the default profile, so the managed schema has to lead.
|
|
650
828
|
*/
|
|
651
|
-
export function apiSchemas(cfg: SupatypeProjectConfig,
|
|
829
|
+
export function apiSchemas(cfg: SupatypeProjectConfig, options?: ApiSchemaOptions): string[] {
|
|
652
830
|
const explicit = cfg.schema?.api_schemas
|
|
653
831
|
// Tier-2 field masking serves from `api`, and the managed schema must come **off** the list: a
|
|
654
832
|
// client picks its schema per request with `Accept-Profile`, so leaving it exposed would let any
|
|
655
833
|
// caller read the unmasked table and make the mask opt-out. The API roles hold no privileges there
|
|
656
834
|
// under tier 2 either, so exposing it would only produce denials.
|
|
657
|
-
const managed = tier === "views" ? "api" : pgSchema(cfg)
|
|
658
|
-
|
|
835
|
+
const managed = options?.tier === "views" ? "api" : pgSchema(cfg)
|
|
836
|
+
// `draft` sits behind the managed schema and never in front of it: the first entry is the default
|
|
837
|
+
// profile, and a client that named no profile reading drafts by default would invert the feature.
|
|
838
|
+
const derived = options?.drafts === true
|
|
839
|
+
? [managed, DRAFT_SCHEMA, ...STACK_API_SCHEMAS]
|
|
840
|
+
: [managed, ...STACK_API_SCHEMAS]
|
|
841
|
+
const list = explicit && explicit.length > 0 ? explicit : derived
|
|
659
842
|
|
|
660
843
|
const seen = new Set<string>()
|
|
661
844
|
const out: string[] = []
|
|
@@ -669,9 +852,88 @@ export function apiSchemas(cfg: SupatypeProjectConfig, tier?: "none" | "extensio
|
|
|
669
852
|
}
|
|
670
853
|
|
|
671
854
|
/** `PGRST_DB_SCHEMA` value: comma-separated, in order. */
|
|
672
|
-
export function apiSchemaList(
|
|
673
|
-
cfg
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
855
|
+
export function apiSchemaList(cfg: SupatypeProjectConfig, options?: ApiSchemaOptions): string {
|
|
856
|
+
return apiSchemas(cfg, options).join(", ")
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Studio roles that see every draft, beyond each record's own creator.
|
|
861
|
+
*
|
|
862
|
+
* All three of them, which is to say anyone with Studio write access. It was narrower, `admin` and
|
|
863
|
+
* `developer` only, and that put two decisions in conflict: whoever may **update** a record may
|
|
864
|
+
* draft and publish it, so an `editor` could edit a record whose pending draft they could not see,
|
|
865
|
+
* and their save would be built on the live row and silently discard a colleague's work. Two editors
|
|
866
|
+
* on one post is the ordinary editorial case, not an exotic one. Seeing and acting line up instead.
|
|
867
|
+
*
|
|
868
|
+
* The creator is not in this list and cannot be removed from it: they wrote the draft, and a system
|
|
869
|
+
* where you cannot read back what you just saved is broken rather than secure. An empty configured
|
|
870
|
+
* list is honoured and means creators only, which is why this cannot fall back on emptiness.
|
|
871
|
+
*
|
|
872
|
+
* `anon` is not a Studio role and can never be here. A draft readable without credentials would make
|
|
873
|
+
* publishing mean nothing.
|
|
874
|
+
*/
|
|
875
|
+
export const DEFAULT_DRAFT_VISIBILITY = ["admin", "developer", "editor"] as const
|
|
876
|
+
|
|
877
|
+
/** Studio roles a Supatype project understands, most privileged first. */
|
|
878
|
+
export const STUDIO_ROLES = ["admin", "developer", "editor"] as const
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* The configured draft-visibility roles, or the default when the project states none.
|
|
882
|
+
*
|
|
883
|
+
* An unknown role name is dropped rather than passed through to a policy, where it would be a role
|
|
884
|
+
* nothing can ever hold: a typo would then read as a working setting that silently grants nobody.
|
|
885
|
+
*/
|
|
886
|
+
export function draftVisibilityRoles(cfg: SupatypeProjectConfig): string[] {
|
|
887
|
+
const declared = cfg.publishing?.draft_visibility
|
|
888
|
+
if (declared === undefined) return [...DEFAULT_DRAFT_VISIBILITY]
|
|
889
|
+
const known = new Set<string>(STUDIO_ROLES)
|
|
890
|
+
const seen = new Set<string>()
|
|
891
|
+
const out: string[] = []
|
|
892
|
+
for (const raw of declared) {
|
|
893
|
+
const role = raw.trim().toLowerCase()
|
|
894
|
+
if (!known.has(role) || seen.has(role)) continue
|
|
895
|
+
seen.add(role)
|
|
896
|
+
out.push(role)
|
|
897
|
+
}
|
|
898
|
+
return out
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/** Bounds on a minted preview link, in seconds. */
|
|
902
|
+
export type PreviewLimits = {
|
|
903
|
+
defaultTtl: number
|
|
904
|
+
maxRecordTtl: number
|
|
905
|
+
maxProjectTtl: number
|
|
906
|
+
allowProjectScope: boolean
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
export const PREVIEW_DEFAULTS: PreviewLimits = {
|
|
910
|
+
defaultTtl: 900,
|
|
911
|
+
maxRecordTtl: 604800,
|
|
912
|
+
maxProjectTtl: 86400,
|
|
913
|
+
allowProjectScope: true,
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* The project's preview-link bounds, defaults filled in.
|
|
918
|
+
*
|
|
919
|
+
* A stated value is clamped to at least a second and, for the default lifetime, to no more than the
|
|
920
|
+
* ceiling it would be issued against: a `default_ttl` above `max_record_ttl` would otherwise mint
|
|
921
|
+
* links that the same config refuses to honour.
|
|
922
|
+
*/
|
|
923
|
+
export function previewLimits(cfg: SupatypeProjectConfig): PreviewLimits {
|
|
924
|
+
const declared = cfg.publishing?.preview
|
|
925
|
+
const positive = (value: number | undefined, fallback: number): number =>
|
|
926
|
+
typeof value === "number" && Number.isFinite(value) && value >= 1 ? Math.floor(value) : fallback
|
|
927
|
+
|
|
928
|
+
const maxRecordTtl = positive(declared?.max_record_ttl, PREVIEW_DEFAULTS.maxRecordTtl)
|
|
929
|
+
const maxProjectTtl = positive(declared?.max_project_ttl, PREVIEW_DEFAULTS.maxProjectTtl)
|
|
930
|
+
return {
|
|
931
|
+
defaultTtl: Math.min(
|
|
932
|
+
positive(declared?.default_ttl, PREVIEW_DEFAULTS.defaultTtl),
|
|
933
|
+
Math.max(maxRecordTtl, maxProjectTtl),
|
|
934
|
+
),
|
|
935
|
+
maxRecordTtl,
|
|
936
|
+
maxProjectTtl,
|
|
937
|
+
allowProjectScope: declared?.allow_project_scope ?? PREVIEW_DEFAULTS.allowProjectScope,
|
|
938
|
+
}
|
|
677
939
|
}
|
package/src/resolve-target.ts
CHANGED
|
@@ -265,7 +265,17 @@ export async function targetSchemaPush(
|
|
|
265
265
|
target: DeployTarget,
|
|
266
266
|
ast: unknown,
|
|
267
267
|
opts?: { force?: boolean; schema?: string; schemaSources?: SchemaSourcesPayload | null },
|
|
268
|
-
): Promise<{
|
|
268
|
+
): Promise<{
|
|
269
|
+
message?: string
|
|
270
|
+
status?: string
|
|
271
|
+
name?: string
|
|
272
|
+
/**
|
|
273
|
+
* What the control plane made of this schema's cache declaration. Absent from an engine push and
|
|
274
|
+
* from a control plane too old to send it, which is why every reader of it has to treat absence
|
|
275
|
+
* as "nothing to say" rather than as an empty declaration.
|
|
276
|
+
*/
|
|
277
|
+
cache?: { tables?: string[]; honoured?: boolean }
|
|
278
|
+
}> {
|
|
269
279
|
if (target.mode === "direct" || (target.mode === "local" && !target.token)) {
|
|
270
280
|
await ensureEngine()
|
|
271
281
|
const body: Record<string, unknown> = {
|
package/src/rest-cache-admin.ts
CHANGED
|
@@ -65,8 +65,14 @@ function formatCacheApiError(
|
|
|
65
65
|
if (status === 403 && trimmed.includes("rest_cache_not_available")) {
|
|
66
66
|
return "REST server cache is not available on this plan or deployment."
|
|
67
67
|
}
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// Both spellings: the server answers "keyspace not configured" now and
|
|
69
|
+
// "valkey not configured" before the rename, and a CLI is routinely newer
|
|
70
|
+
// than the deployment it is pointed at.
|
|
71
|
+
if (status === 503 && (trimmed.includes("keyspace") || trimmed.includes("valkey"))) {
|
|
72
|
+
return (
|
|
73
|
+
"No cache server is configured — server-side REST caching needs one. " +
|
|
74
|
+
"`supatype dev` starts it for you; on a self-hosted stack, check the `db` or `valkey` service."
|
|
75
|
+
)
|
|
70
76
|
}
|
|
71
77
|
return `${operation} failed (${status}): ${trimmed || "(empty body)"}`
|
|
72
78
|
}
|
package/src/schema-ast-v2.ts
CHANGED
|
@@ -95,6 +95,8 @@ export interface DbFieldAnnotations {
|
|
|
95
95
|
export interface PlatformFieldAnnotations {
|
|
96
96
|
editor?: string
|
|
97
97
|
readOnly?: boolean
|
|
98
|
+
/** Studio searches this column in the list view. Set by `Searchable<T>`. */
|
|
99
|
+
searchable?: boolean
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
export interface FieldAnnotations {
|
|
@@ -143,6 +145,18 @@ export interface KernelFieldFacts {
|
|
|
143
145
|
index?: boolean
|
|
144
146
|
}
|
|
145
147
|
|
|
148
|
+
/**
|
|
149
|
+
* The `cache` declaration as it reaches the AST. See `ModelMeta.cache` in `@supatype/types` for
|
|
150
|
+
* what each setting means; this is the wire shape, kept narrow so a typo in the extractor is a
|
|
151
|
+
* type error rather than an extra key nobody reads.
|
|
152
|
+
*/
|
|
153
|
+
export interface ModelCacheAst {
|
|
154
|
+
enabled?: boolean
|
|
155
|
+
maxTtl?: number
|
|
156
|
+
public?: boolean
|
|
157
|
+
rows?: boolean
|
|
158
|
+
}
|
|
159
|
+
|
|
146
160
|
/** Internal parse result: not serialized. */
|
|
147
161
|
export interface ParsedField {
|
|
148
162
|
kind: FieldKind
|
|
@@ -173,6 +187,9 @@ export interface ModelAstV2 {
|
|
|
173
187
|
access: Record<string, unknown>
|
|
174
188
|
hooks?: Record<string, unknown>
|
|
175
189
|
validate?: Record<string, unknown>
|
|
190
|
+
/** Columns Studio's list view searches. See {@link emitModel}. */
|
|
191
|
+
searchFields?: string[]
|
|
192
|
+
cache?: ModelCacheAst
|
|
176
193
|
}
|
|
177
194
|
}
|
|
178
195
|
}
|
|
@@ -193,6 +210,21 @@ export interface ExtractedSchemaAstV2 {
|
|
|
193
210
|
storageBuckets?: ExtractedStorageBucketAst[]
|
|
194
211
|
locales?: string[]
|
|
195
212
|
defaultLocale?: string
|
|
213
|
+
/**
|
|
214
|
+
* Draft visibility and preview-link bounds, from `supatype.config.ts`.
|
|
215
|
+
*
|
|
216
|
+
* Not extracted from the schema like everything else here: it is a project setting that the
|
|
217
|
+
* engine needs at the moment it compiles the draft policies, so it rides the same payload rather
|
|
218
|
+
* than growing a delivery path of its own. Attached by `withPublishing`, and absent for a project
|
|
219
|
+
* with no versioned model.
|
|
220
|
+
*/
|
|
221
|
+
publishing?: {
|
|
222
|
+
draftVisibility: string[]
|
|
223
|
+
previewDefaultTtl: number
|
|
224
|
+
previewMaxRecordTtl: number
|
|
225
|
+
previewMaxProjectTtl: number
|
|
226
|
+
previewAllowProjectScope: boolean
|
|
227
|
+
}
|
|
196
228
|
}
|
|
197
229
|
|
|
198
230
|
const DEFAULT_DB_BY_KIND: Partial<Record<FieldKind, Partial<DbFieldAnnotations>>> = {
|
|
@@ -391,6 +423,8 @@ export function emitModel(
|
|
|
391
423
|
hooks: Record<string, unknown> = {},
|
|
392
424
|
constraints: unknown[] = [],
|
|
393
425
|
validators: Record<string, unknown> = {},
|
|
426
|
+
searchFields: string[] = [],
|
|
427
|
+
cache?: ModelCacheAst | undefined,
|
|
394
428
|
): ModelAstV2 {
|
|
395
429
|
return {
|
|
396
430
|
name,
|
|
@@ -404,10 +438,23 @@ export function emitModel(
|
|
|
404
438
|
// supatype-server reads them, Postgres never sees them.
|
|
405
439
|
// Validators sit in `platform` beside `hooks`: both are enforced by the API layer on the
|
|
406
440
|
// write path, and neither is something Postgres knows about.
|
|
441
|
+
// `searchFields` sits in `platform` because it is an admin-UI concern: Studio's list view
|
|
442
|
+
// renders its search box when a model has them and filters on the first. Postgres knows
|
|
443
|
+
// nothing about it, which is why it is not in `db` beside `indexes` — searching a column and
|
|
444
|
+
// indexing one are different asks, and `Indexed` already covers the latter.
|
|
407
445
|
platform: {
|
|
408
446
|
access,
|
|
409
447
|
...(Object.keys(hooks).length > 0 && { hooks }),
|
|
410
448
|
...(Object.keys(validators).length > 0 && { validate: validators }),
|
|
449
|
+
...(searchFields.length > 0 && { searchFields }),
|
|
450
|
+
// Cache sits in `platform` beside `hooks` for the same reason, and travels the same way:
|
|
451
|
+
// the route manifest, not this AST. The schema engine re-serialises its own parsed struct
|
|
452
|
+
// when it writes `ast_snapshot`, and its `PlatformModelAnnotations` knows only `access`
|
|
453
|
+
// and `searchFields` — so this key, like `hooks` above it, does not survive that round
|
|
454
|
+
// trip and the manifest is what the server actually reads. Kept here because the AST is
|
|
455
|
+
// the schema's own description of itself, and a reader looking for the declaration should
|
|
456
|
+
// find it next to the other API-layer concerns.
|
|
457
|
+
...(cache !== undefined && { cache }),
|
|
411
458
|
},
|
|
412
459
|
},
|
|
413
460
|
}
|