@polardbx/pxbaas 0.1.0-beta.1 → 0.1.0-beta.3
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/README.md +179 -0
- package/dist/app/preflight.d.ts +12 -1
- package/dist/app/preflight.d.ts.map +1 -1
- package/dist/app/preflight.js +28 -5
- package/dist/app/preflight.js.map +1 -1
- package/dist/app/spec-lint.d.ts.map +1 -1
- package/dist/app/spec-lint.js +16 -0
- package/dist/app/spec-lint.js.map +1 -1
- package/dist/commands/app/index.d.ts.map +1 -1
- package/dist/commands/app/index.js +253 -12
- package/dist/commands/app/index.js.map +1 -1
- package/dist/commands/db/index.d.ts +1 -0
- package/dist/commands/db/index.d.ts.map +1 -1
- package/dist/commands/db/index.js +74 -27
- package/dist/commands/db/index.js.map +1 -1
- package/dist/commands/gen/index.d.ts.map +1 -1
- package/dist/commands/gen/index.js +13 -10
- package/dist/commands/gen/index.js.map +1 -1
- package/dist/commands/login/index.d.ts.map +1 -1
- package/dist/commands/login/index.js +9 -34
- package/dist/commands/login/index.js.map +1 -1
- package/dist/commands/profile/index.d.ts.map +1 -1
- package/dist/commands/profile/index.js +3 -0
- package/dist/commands/profile/index.js.map +1 -1
- package/dist/credentials/index.d.ts +2 -0
- package/dist/credentials/index.d.ts.map +1 -1
- package/dist/credentials/index.js +2 -0
- package/dist/credentials/index.js.map +1 -1
- package/dist/db/apply-schema.d.ts +98 -11
- package/dist/db/apply-schema.d.ts.map +1 -1
- package/dist/db/apply-schema.js +247 -80
- package/dist/db/apply-schema.js.map +1 -1
- package/dist/db/introspect.d.ts +54 -0
- package/dist/db/introspect.d.ts.map +1 -0
- package/dist/db/introspect.js +140 -0
- package/dist/db/introspect.js.map +1 -0
- package/dist/db/sql-file.d.ts +55 -0
- package/dist/db/sql-file.d.ts.map +1 -1
- package/dist/db/sql-file.js +417 -0
- package/dist/db/sql-file.js.map +1 -1
- package/package.json +2 -2
- package/skills/pxbaas-app-spec/references/runtime-contract.md +1 -1
- package/skills/pxbaas-app-spec/references/templates-examples.md +5 -2
- package/skills/pxbaas-cli/SKILL.md +7 -4
- package/skills/pxbaas-cli/references/app-deploy.md +29 -2
- package/skills/pxbaas-cli/references/auth-and-credentials.md +14 -2
- package/skills/pxbaas-cli/references/data-postgrest.md +15 -0
- package/skills/pxbaas-cli/references/db-and-meta.md +12 -0
- package/skills/pxbaas-cli/references/multi-tenant-apps.md +35 -4
- package/templates/webapp/csr-bff/db/rls.json +13 -0
- package/templates/webapp/csr-bff/db/schema.sql +8 -28
- package/templates/webapp/manifest.json +6 -3
- package/templates/webapp/react-vite-hono/backend/index.ts +21 -4
- package/templates/webapp/ssr-blog/db/rls.json +13 -0
- package/templates/webapp/ssr-blog/db/schema.sql +8 -28
- package/templates/webapp/ssr-hono/backend/index.ts +50 -31
- package/templates/webapp/ssr-shop/db/rls.json +16 -0
- package/templates/webapp/ssr-shop/db/schema.sql +12 -42
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Credentials, Environment & User Management
|
|
2
2
|
|
|
3
|
-
Covers: `login` (human-only) · `logout` · `whoami` · `status` · `doctor` · `profile` · `auth users`.
|
|
3
|
+
Covers: `login` (human-only) · `profile import` (agent-runnable, env-file) · `logout` · `whoami` · `status` · `doctor` · `profile` · `auth users`.
|
|
4
4
|
|
|
5
5
|
## login — agents must NEVER run this
|
|
6
6
|
|
|
@@ -11,7 +11,18 @@ pxbaas login --endpoint <url> --tenant-id <id> --anon-key-stdin --service-key-st
|
|
|
11
11
|
- **Requires an interactive TTY** (it prompts via inquirer); agents cannot complete it. **Do not attempt.**
|
|
12
12
|
- Keys are read from stdin via `--anon-key-stdin` / `--service-key-stdin` so they never land in shell history.
|
|
13
13
|
- Credentials are stored in the OS keyring (keytar), falling back to a `0600` file when no keyring is available.
|
|
14
|
-
- If another command reports `No active profile. Use "pxbaas login" ...` → **ask the user to log in manually
|
|
14
|
+
- If another command reports `No active profile. Use "pxbaas login" ...` → **ask the user to log in manually**, OR — if a Supabase/BaaS `.env` file for the instance is available — bind it non-interactively with `pxbaas profile import <env-file>` (see below; that one IS agent-runnable).
|
|
15
|
+
|
|
16
|
+
## profile import — bind a single-tenant instance from a `.env` file (agents MAY run this)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pxbaas profile import <env-file> --name <profile> [--intranet] --json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- Reads `SUPABASE_URL` (fallback `SUPABASE_PUBLIC_URL` / `SUPABASE_INTRANET_URL`), `SUPABASE_ANON_KEY` (fallback `ANON_KEY` / `GLOBAL_ANON_KEY`) and `SUPABASE_SERVICE_ROLE_KEY` (fallback `SERVICE_ROLE_KEY` / `GLOBAL_SERVICE_ROLE_KEY`) from the file, validates the JWTs (rejects expired keys), then writes the profile + credentials and switches current to it.
|
|
23
|
+
- **Agent-runnable**: keys come from a file, never from argv/shell history, so no TTY is needed. Prefer this over `login` when you already have the instance `.env`.
|
|
24
|
+
- `--intranet` prefers `SUPABASE_INTRANET_URL` (VPC-internal entrypoint) over the public URL. `--name` sets the profile name (default: global `--profile`, else `default`); an existing profile of the same name is **overwritten**.
|
|
25
|
+
- **Single-tenant only for now.** A `MULTI_TENANT_MODE=true` env file is rejected with guidance — its global keys cannot access tenant resources; use `pxbaas login` with per-tenant anon/service keys instead.
|
|
15
26
|
|
|
16
27
|
## Verify login state (run these first)
|
|
17
28
|
|
|
@@ -65,6 +76,7 @@ pxbaas logout # clears credentials for the current profile
|
|
|
65
76
|
pxbaas profile ls --json # list all profiles
|
|
66
77
|
pxbaas profile use <name> --json # switch current profile
|
|
67
78
|
pxbaas profile rm <name> --json # remove a profile (destructive to local config — confirm first)
|
|
79
|
+
pxbaas profile import <env-file> --name <profile> --json # bind a single-tenant instance from a .env file (agent-runnable; see above)
|
|
68
80
|
```
|
|
69
81
|
|
|
70
82
|
- You can also pass global `--profile <name>` to target one environment without changing the default.
|
|
@@ -83,3 +83,18 @@ pxbaas rest POST "/todos" --data '{"title":"x"}' --role anon --json
|
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
> REST endpoint details, embedding/RPC constraints: fetch `${endpoint}/docs` — see api-reference.md.
|
|
86
|
+
|
|
87
|
+
## Schema cache after DDL
|
|
88
|
+
|
|
89
|
+
The REST layer (`/rest/v1`) caches each table's column list. After a DDL change
|
|
90
|
+
(`ALTER TABLE ... ADD/DROP/MODIFY COLUMN`, or `CREATE`/`DROP TABLE`) the cache
|
|
91
|
+
re-introspects automatically: immediately where the platform wires DDL to a
|
|
92
|
+
cache reload, otherwise within one refresh interval (~30s). Until then:
|
|
93
|
+
|
|
94
|
+
- `select=*` (and `Prefer: return=representation` bodies) may still omit a
|
|
95
|
+
just-added column. This is a stale column list, not an error — the row is
|
|
96
|
+
returned, only the new field is missing.
|
|
97
|
+
- Workaround: enumerate columns explicitly (`?select=id,room_id,board`) instead
|
|
98
|
+
of `*`, or retry after a few seconds.
|
|
99
|
+
|
|
100
|
+
`pxbaas db add-column` / `db create-table` print a reminder about this.
|
|
@@ -19,6 +19,18 @@ pxbaas db query "SELECT count(*) FROM users" --role anon --json
|
|
|
19
19
|
{ "rows": [ { "count(*)": 42 } ], "rowCount": 1, "fields": [ { "name": "count(*)", "dataType": "BIGINT" } ] }
|
|
20
20
|
```
|
|
21
21
|
- **Gotchas:** `db query` can also run write/DDL SQL. If the statement contains INSERT/UPDATE/DELETE/DROP/ALTER/CREATE, treat it as destructive and confirm with the user first.
|
|
22
|
+
- **Multi-tenant:** with an MT profile (`--tenant-id`), `db query` routes through `/api/tenant-ddl/query` using the per-tenant service_role key and runs on the tenant's OWN database (`/pg/query` is platform-admin only and rejects per-tenant keys). `--role anon` is not applicable there — the tenant-ddl endpoint is service_role-only, so the query always runs as the tenant service_role. For batch DDL + RLS use `db push <file>` or `app db-init <name>` (see app-deploy.md); both are MT-aware.
|
|
23
|
+
|
|
24
|
+
## db push — apply a SQL file (DDL + RLS + seed)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pxbaas db push ./db/schema.sql --schema public --dry-run # preview the plan
|
|
28
|
+
pxbaas db push ./db/schema.sql --yes # apply (stop-on-first-error, no rollback)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- **Flags:** `--schema <name>` (single-tenant `__SCHEMA__` target, default `public`) · `--rls <file>` (declarative `db/rls.json`) · `--role` (default service_role) · `--dry-run` · `--yes` · `--json`
|
|
32
|
+
- Mode-aware: single-tenant runs through `/pg/query`; multi-tenant through `/api/tenant-ddl` (de-qualifies `__SCHEMA__`, diverts RLS to the structured endpoints). Shows the full plan and requires authorization; never rolls back.
|
|
33
|
+
- `app db-init <name>` is the app-bundled convenience wrapper (reads `<appDir>/db/schema.sql` + `db/rls.json`).
|
|
22
34
|
|
|
23
35
|
## db tables — list tables
|
|
24
36
|
|
|
@@ -12,14 +12,20 @@ most common source of 401/403 confusion.
|
|
|
12
12
|
| `/api/platform/*` (Studio: tenants CRUD) | **dashboard Basic Auth** (`DASHBOARD_USERNAME`/`DASHBOARD_PASSWORD`) | global service key → `401 Unauthorized` |
|
|
13
13
|
| `/pg/query` (ad-hoc SQL) | **global service_role key**, sent as BOTH `Authorization: Bearer` and `apikey` | Basic Auth → 401 "apikey or Bearer required"; per-tenant key → `401` |
|
|
14
14
|
| Data plane `/rest/v1`, `/auth/v1`; Management API `/v1/projects/default/*` (apps create/publish, functions deploy) | **per-tenant key** (Management API also needs `X-Tenant-Id`) | global key + `X-Tenant-Id` → `403 Global credentials cannot access tenant resources` |
|
|
15
|
+
| `/api/tenant-ddl/*` (tenant self-service DDL + RLS) | **per-tenant service_role key** — send only `Authorization: Bearer`; Kong verifies the JWT and injects the trusted `X-Tenant-Id` (never send it yourself) | global key → `403 GLOBAL_KEY_NOT_ALLOWED`; per-tenant anon → `403 ROLE_NOT_ALLOWED`; suspended tenant → `403 TENANT_NOT_ACTIVE` |
|
|
16
|
+
|
|
17
|
+
> RLS registry management is symmetric: `rls/tables` and `rls/policies` both support POST/GET/DELETE
|
|
18
|
+
> (DELETE on `rls/tables` does not require the table to still exist, so it cleans up after a DROP).
|
|
15
19
|
|
|
16
20
|
- A per-tenant anon/service key carries a `tenant_id` claim, so **REST and Auth route from the
|
|
17
21
|
key alone** — `X-Tenant-Id` is optional there. Issued user JWTs carry `aud=<tenant_id>`.
|
|
18
22
|
- Using tenant X's key with `X-Tenant-Id: Y` → `403 Token tenant mismatch` (gateway guardrail).
|
|
19
|
-
- CLI: `pxbaas login --endpoint <url> --tenant-id <id>` with the tenant's keys gives a
|
|
20
|
-
MT profile
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
- CLI: `pxbaas login --endpoint <url> --tenant-id <id>` with the tenant's per-tenant keys gives a
|
|
24
|
+
working MT profile. As of the tenant self-service DDL capability, **`pxbaas db query`, `db push`,
|
|
25
|
+
and `app db-init` all work in MT** — they route through `/api/tenant-ddl` with the per-tenant
|
|
26
|
+
service_role key (DDL runs on the tenant's own db; the tenant MySQL account's `GRANT ALL ON
|
|
27
|
+
tenant_<id>.*` is the hard isolation boundary). The old "`db query` 401s in MT, use the global key
|
|
28
|
+
on `/pg/query`" guidance is obsolete; `/pg/query` remains platform-admin only.
|
|
23
29
|
|
|
24
30
|
## 2. Provisioning a tenant
|
|
25
31
|
|
|
@@ -63,6 +69,31 @@ curl -sS -o /dev/null -w '%{http_code}\n' -X POST "$ENDPOINT/rest/v1/posts" \
|
|
|
63
69
|
-d '{"title":"x","author_id":"someone-else","status":"draft"}' # expect 403 PGRST302
|
|
64
70
|
```
|
|
65
71
|
|
|
72
|
+
## 3b. Tenant self-service DDL / db-init as a normal tenant
|
|
73
|
+
|
|
74
|
+
A normal tenant (holding only its per-tenant service_role key — no platform admin credential) can
|
|
75
|
+
initialize and tear down its own app database through the CLI:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pxbaas login --endpoint <url> --tenant-id <id> # paste the tenant's per-tenant anon + service keys
|
|
79
|
+
pxbaas app db-init <name> --dir ./apps --dry-run # preview the plan (DDL + seed + RLS)
|
|
80
|
+
pxbaas app db-init <name> --dir ./apps --yes # apply (stop-on-first-error, no rollback)
|
|
81
|
+
pxbaas db query "SELECT count(*) FROM posts" --json
|
|
82
|
+
pxbaas app destroy <name> --confirm --purge-db # optional: also drop the app's tables + RLS rows
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- `db-init`/`db push` read `db/schema.sql` (DDL + seed) and the declarative `db/rls.json` (RLS).
|
|
86
|
+
In MT the `__SCHEMA__` qualifier is dropped (the connection is already bound to the tenant db) and
|
|
87
|
+
cross-schema names are refused by GRANT.
|
|
88
|
+
- **RLS is registered through the structured `/api/tenant-ddl/rls/*` endpoints**, not raw central
|
|
89
|
+
INSERTs (the tenant account has no GRANT on the shared `supabase` db). `table_schema` is bound
|
|
90
|
+
server-side to the verified tenant db — the CLI never sends it, so a tenant cannot touch another's
|
|
91
|
+
policies. If `db/rls.json` is absent, RLS INSERTs embedded in `schema.sql` are parsed and routed
|
|
92
|
+
the same way.
|
|
93
|
+
- `--purge-db` DROPs the app's tables and DELETEs its RLS registry rows — both the policies and the
|
|
94
|
+
central `_mysqlrest_rls_tables` toggle (via `DELETE /api/tenant-ddl/rls/tables`). On instances that
|
|
95
|
+
predate that endpoint the toggle row is left (harmless; re-created on the next `db-init`).
|
|
96
|
+
|
|
66
97
|
## 4. Deploying a webapp into a tenant
|
|
67
98
|
|
|
68
99
|
- Every Management API call needs `Authorization: Bearer <tenant service_role_key>` **and**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Declarative RLS for the CSR/BFF blog webapp. Applied by `pxbaas app db-init` (single-tenant AND multi-tenant). table_schema is NEVER declared here: single-tenant binds it to --schema (default public); multi-tenant binds it server-side to the tenant db.",
|
|
3
|
+
"tables": [
|
|
4
|
+
{ "table": "posts", "rlsEnabled": true, "rlsForced": false }
|
|
5
|
+
],
|
|
6
|
+
"policies": [
|
|
7
|
+
{ "table": "posts", "name": "posts_select_anon", "role": "anon", "command": "SELECT", "using": "`status` = 'published'", "check": null, "permissive": true },
|
|
8
|
+
{ "table": "posts", "name": "posts_select_auth", "role": "authenticated", "command": "SELECT", "using": "`status` = 'published' OR author_id = {{request.jwt.claim.sub}}", "check": null, "permissive": true },
|
|
9
|
+
{ "table": "posts", "name": "posts_insert_auth", "role": "authenticated", "command": "INSERT", "using": null, "check": "author_id = {{request.jwt.claim.sub}}", "permissive": true },
|
|
10
|
+
{ "table": "posts", "name": "posts_update_auth", "role": "authenticated", "command": "UPDATE", "using": "author_id = {{request.jwt.claim.sub}}", "check": "author_id = {{request.jwt.claim.sub}}", "permissive": true },
|
|
11
|
+
{ "table": "posts", "name": "posts_delete_auth", "role": "authenticated", "command": "DELETE", "using": "author_id = {{request.jwt.claim.sub}}", "check": null, "permissive": true }
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
-- · 多租户实例:__SCHEMA__ = tenant_<tenant_id>
|
|
7
7
|
-- sed 's/__SCHEMA__/public/g' db/schema.sql
|
|
8
8
|
--
|
|
9
|
-
--
|
|
10
|
-
--
|
|
9
|
+
-- 执行通道(推荐):`pxbaas app db-init <name>` —— 单/多租户通用:
|
|
10
|
+
-- 单租户 __SCHEMA__→public 经 /pg/query;多租户 __SCHEMA__ 去限定经 /api/tenant-ddl
|
|
11
|
+
-- 用 per-tenant service_role key 执行(GRANT 仅本租户库)。本文件只含 DDL + 种子;
|
|
12
|
+
-- RLS 声明在同目录 db/rls.json,由 db-init 一并应用。
|
|
11
13
|
--
|
|
12
|
-
-- ⚠️ RLS 元数据落点(写错静默失效=RLS 关闭):永远写中心库 supabase._mysqlrest_rls_
|
|
13
|
-
--
|
|
14
|
+
-- ⚠️ RLS 元数据落点(写错静默失效=RLS 关闭):永远写中心库 supabase._mysqlrest_rls_*;
|
|
15
|
+
-- db-init 自动绑定 table_schema(单租户=--schema,多租户=服务端强绑),勿手写库名。
|
|
14
16
|
-- =============================================================================
|
|
15
17
|
|
|
16
18
|
CREATE TABLE IF NOT EXISTS `__SCHEMA__`.`posts` (
|
|
@@ -24,30 +26,8 @@ CREATE TABLE IF NOT EXISTS `__SCHEMA__`.`posts` (
|
|
|
24
26
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
25
27
|
);
|
|
26
28
|
|
|
27
|
-
-- ---- RLS
|
|
28
|
-
|
|
29
|
-
VALUES ('__SCHEMA__','posts',1,0);
|
|
30
|
-
|
|
31
|
-
-- ---- RLS 策略:anon 只读 published;authenticated 读 published 或本人;写改删限本人 ----
|
|
32
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
33
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
34
|
-
VALUES ('__SCHEMA__','posts','posts_select_anon','anon','SELECT','`status` = ''published''',NULL,1);
|
|
35
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
36
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
37
|
-
VALUES ('__SCHEMA__','posts','posts_select_auth','authenticated','SELECT',
|
|
38
|
-
'`status` = ''published'' OR author_id = {{request.jwt.claim.sub}}',NULL,1);
|
|
39
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
40
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
41
|
-
VALUES ('__SCHEMA__','posts','posts_insert_auth','authenticated','INSERT',NULL,
|
|
42
|
-
'author_id = {{request.jwt.claim.sub}}',1);
|
|
43
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
44
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
45
|
-
VALUES ('__SCHEMA__','posts','posts_update_auth','authenticated','UPDATE',
|
|
46
|
-
'author_id = {{request.jwt.claim.sub}}','author_id = {{request.jwt.claim.sub}}',1);
|
|
47
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
48
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
49
|
-
VALUES ('__SCHEMA__','posts','posts_delete_auth','authenticated','DELETE',
|
|
50
|
-
'author_id = {{request.jwt.claim.sub}}',NULL,1);
|
|
29
|
+
-- ---- RLS 注册与策略:声明在同目录 db/rls.json,由 `pxbaas app db-init` 应用 ----
|
|
30
|
+
-- anon 只读 published;authenticated 读 published 或本人;写/改/删限本人(author_id = jwt.sub)。
|
|
51
31
|
|
|
52
32
|
-- ---- 种子文章(3 篇 published,幂等)----
|
|
53
33
|
INSERT INTO `__SCHEMA__`.`posts` (title, content, author_id, author_name, status)
|
|
@@ -163,6 +163,7 @@
|
|
|
163
163
|
"README.md",
|
|
164
164
|
"backend/index.ts",
|
|
165
165
|
"db/schema.sql",
|
|
166
|
+
"db/rls.json",
|
|
166
167
|
"frontend/dist/index.html"
|
|
167
168
|
],
|
|
168
169
|
"frontend": {
|
|
@@ -182,7 +183,7 @@
|
|
|
182
183
|
},
|
|
183
184
|
"ssrProbePath": "/posts",
|
|
184
185
|
"nextSteps": [
|
|
185
|
-
"Initialize the database BEFORE deploy: run `pxbaas app db-init <name> --dir <appsDir
|
|
186
|
+
"Initialize the database BEFORE deploy: run `pxbaas app db-init <name> --dir <appsDir>`. It reads db/schema.sql (DDL + seed) and db/rls.json (RLS registry), shows the full plan, and applies it only after you authorize (--dry-run to preview, --yes for non-interactive). Works in single-tenant (/pg/query) AND multi-tenant (/api/tenant-ddl with your per-tenant service_role key); the CLI never auto-runs DDL.",
|
|
186
187
|
"Requires capabilities.sessionMode=proxy-injected-header.",
|
|
187
188
|
"Data-plane base and anon key come from the platform-in SUPABASE_INTERNAL_URL / SUPABASE_ANON_KEY (edge-runtime MR #30497903); no plaintext key editing is needed."
|
|
188
189
|
]
|
|
@@ -196,6 +197,7 @@
|
|
|
196
197
|
"README.md",
|
|
197
198
|
"backend/index.ts",
|
|
198
199
|
"db/schema.sql",
|
|
200
|
+
"db/rls.json",
|
|
199
201
|
"frontend/dist/admin.html",
|
|
200
202
|
"frontend/dist/article.html",
|
|
201
203
|
"frontend/dist/blog.js",
|
|
@@ -222,7 +224,7 @@
|
|
|
222
224
|
"session": "proxy-injected"
|
|
223
225
|
},
|
|
224
226
|
"nextSteps": [
|
|
225
|
-
"Initialize the database BEFORE deploy: run `pxbaas app db-init <name> --dir <appsDir
|
|
227
|
+
"Initialize the database BEFORE deploy: run `pxbaas app db-init <name> --dir <appsDir>`. It reads db/schema.sql (DDL + seed) and db/rls.json (RLS registry), shows the full plan, and applies it only after you authorize (--dry-run to preview, --yes for non-interactive). Works in single-tenant (/pg/query) AND multi-tenant (/api/tenant-ddl with your per-tenant service_role key); the CLI never auto-runs DDL.",
|
|
226
228
|
"Requires capabilities.sessionMode=proxy-injected-header.",
|
|
227
229
|
"The BFF forwards to the platform-injected SUPABASE_INTERNAL_URL with SUPABASE_ANON_KEY (edge-runtime MR #30497903); no plaintext key editing is needed."
|
|
228
230
|
]
|
|
@@ -236,6 +238,7 @@
|
|
|
236
238
|
"README.md",
|
|
237
239
|
"backend/index.ts",
|
|
238
240
|
"db/schema.sql",
|
|
241
|
+
"db/rls.json",
|
|
239
242
|
"frontend/dist/index.html"
|
|
240
243
|
],
|
|
241
244
|
"frontend": {
|
|
@@ -255,7 +258,7 @@
|
|
|
255
258
|
},
|
|
256
259
|
"ssrProbePath": "/shop",
|
|
257
260
|
"nextSteps": [
|
|
258
|
-
"Initialize the database BEFORE deploy: run `pxbaas app db-init <name> --dir <appsDir
|
|
261
|
+
"Initialize the database BEFORE deploy: run `pxbaas app db-init <name> --dir <appsDir>`. It reads db/schema.sql (DDL + seed) and db/rls.json (RLS: products public-read; orders and order_items owner-gated by buyer_email = jwt.email), shows the full plan, and applies it only after you authorize (--dry-run to preview, --yes for non-interactive). Works in single-tenant (/pg/query) AND multi-tenant (/api/tenant-ddl with your per-tenant service_role key); the CLI never auto-runs DDL.",
|
|
259
262
|
"Create the admin account (admin@shop.demo) via GoTrue signup if the admin whitelist is needed.",
|
|
260
263
|
"Data-plane base and anon key come from the platform-injected SUPABASE_INTERNAL_URL / SUPABASE_ANON_KEY (edge-runtime MR #30497903); no plaintext key editing is needed. Requires the instance to report react/react-dom in capabilities.ssrLibs."
|
|
261
264
|
]
|
|
@@ -10,10 +10,27 @@ app.use("*", async (context, next) => {
|
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
app.options("*", (context) => context.body(null, 204));
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
)
|
|
13
|
+
|
|
14
|
+
// Path contract (APP_WORKER_PATH_REWRITE off = current default): the proxy
|
|
15
|
+
// forwards /apps/{mount}/api/<x> to the worker as /<candidateSlug>/<x>, so a
|
|
16
|
+
// literal app.get("/health") never matches at runtime. Normalize the candidate
|
|
17
|
+
// slug prefix away and dispatch on the logical path (same convention as the
|
|
18
|
+
// ssr-shop / ssr-blog / csr-bff templates).
|
|
19
|
+
function norm(path: string): string {
|
|
20
|
+
return path.replace(/^\/app_[^/]+/, "") || path;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
app.get("*", (context) => {
|
|
24
|
+
const p = norm(new URL(context.req.url).pathname);
|
|
25
|
+
if (p === "/health" || p === "/api/health") {
|
|
26
|
+
return context.json({ ok: true, app: "{{SLUG}}" });
|
|
27
|
+
}
|
|
28
|
+
if (p === "/hello" || p === "/api/hello") {
|
|
29
|
+
return context.json({ message: "Hello from {{APP_NAME}}" });
|
|
30
|
+
}
|
|
31
|
+
return context.json({ code: "NOT_FOUND", message: "Route not found", path: p }, 404);
|
|
32
|
+
});
|
|
33
|
+
|
|
17
34
|
app.notFound((context) =>
|
|
18
35
|
context.json({ code: "NOT_FOUND", message: "Route not found" }, 404),
|
|
19
36
|
);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Declarative RLS for the SSR blog webapp. Applied by `pxbaas app db-init` (single-tenant AND multi-tenant). table_schema is NEVER declared here: single-tenant binds it to --schema (default public); multi-tenant binds it server-side to the tenant db.",
|
|
3
|
+
"tables": [
|
|
4
|
+
{ "table": "posts_ssr", "rlsEnabled": true, "rlsForced": false }
|
|
5
|
+
],
|
|
6
|
+
"policies": [
|
|
7
|
+
{ "table": "posts_ssr", "name": "posts_ssr_select_anon", "role": "anon", "command": "SELECT", "using": "`status` = 'published'", "check": null, "permissive": true },
|
|
8
|
+
{ "table": "posts_ssr", "name": "posts_ssr_select_auth", "role": "authenticated", "command": "SELECT", "using": "`status` = 'published' OR author_id = {{request.jwt.claim.sub}}", "check": null, "permissive": true },
|
|
9
|
+
{ "table": "posts_ssr", "name": "posts_ssr_insert_auth", "role": "authenticated", "command": "INSERT", "using": null, "check": "author_id = {{request.jwt.claim.sub}}", "permissive": true },
|
|
10
|
+
{ "table": "posts_ssr", "name": "posts_ssr_update_auth", "role": "authenticated", "command": "UPDATE", "using": "author_id = {{request.jwt.claim.sub}}", "check": "author_id = {{request.jwt.claim.sub}}", "permissive": true },
|
|
11
|
+
{ "table": "posts_ssr", "name": "posts_ssr_delete_auth", "role": "authenticated", "command": "DELETE", "using": "author_id = {{request.jwt.claim.sub}}", "check": null, "permissive": true }
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
-- · 多租户实例:__SCHEMA__ = tenant_<tenant_id>
|
|
7
7
|
-- sed 's/__SCHEMA__/public/g' db/schema.sql
|
|
8
8
|
--
|
|
9
|
-
--
|
|
10
|
-
--
|
|
9
|
+
-- 执行通道(推荐):`pxbaas app db-init <name>` —— 单/多租户通用:
|
|
10
|
+
-- 单租户 __SCHEMA__→public 经 /pg/query;多租户 __SCHEMA__ 去限定经 /api/tenant-ddl
|
|
11
|
+
-- 用 per-tenant service_role key 执行(GRANT 仅本租户库)。本文件只含 DDL + 种子;
|
|
12
|
+
-- RLS 声明在同目录 db/rls.json,由 db-init 一并应用。
|
|
11
13
|
--
|
|
12
|
-
-- ⚠️ RLS 元数据落点(写错静默失效=RLS 关闭):永远写中心库 supabase._mysqlrest_rls_
|
|
13
|
-
--
|
|
14
|
+
-- ⚠️ RLS 元数据落点(写错静默失效=RLS 关闭):永远写中心库 supabase._mysqlrest_rls_*;
|
|
15
|
+
-- db-init 自动绑定 table_schema(单租户=--schema,多租户=服务端强绑),勿手写库名。
|
|
14
16
|
-- =============================================================================
|
|
15
17
|
|
|
16
18
|
CREATE TABLE IF NOT EXISTS `__SCHEMA__`.`posts_ssr` (
|
|
@@ -26,30 +28,8 @@ CREATE TABLE IF NOT EXISTS `__SCHEMA__`.`posts_ssr` (
|
|
|
26
28
|
KEY `idx_posts_ssr_status` (`status`, `created_at`)
|
|
27
29
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
28
30
|
|
|
29
|
-
-- ---- RLS
|
|
30
|
-
|
|
31
|
-
VALUES ('__SCHEMA__','posts_ssr',1,0);
|
|
32
|
-
|
|
33
|
-
-- ---- RLS 策略:anon 只读 published;authenticated 读 published 或本人;写改删限本人 ----
|
|
34
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
35
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
36
|
-
VALUES ('__SCHEMA__','posts_ssr','posts_ssr_select_anon','anon','SELECT','`status` = ''published''',NULL,1);
|
|
37
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
38
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
39
|
-
VALUES ('__SCHEMA__','posts_ssr','posts_ssr_select_auth','authenticated','SELECT',
|
|
40
|
-
'`status` = ''published'' OR author_id = {{request.jwt.claim.sub}}',NULL,1);
|
|
41
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
42
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
43
|
-
VALUES ('__SCHEMA__','posts_ssr','posts_ssr_insert_auth','authenticated','INSERT',NULL,
|
|
44
|
-
'author_id = {{request.jwt.claim.sub}}',1);
|
|
45
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
46
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
47
|
-
VALUES ('__SCHEMA__','posts_ssr','posts_ssr_update_auth','authenticated','UPDATE',
|
|
48
|
-
'author_id = {{request.jwt.claim.sub}}','author_id = {{request.jwt.claim.sub}}',1);
|
|
49
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
50
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
51
|
-
VALUES ('__SCHEMA__','posts_ssr','posts_ssr_delete_auth','authenticated','DELETE',
|
|
52
|
-
'author_id = {{request.jwt.claim.sub}}',NULL,1);
|
|
31
|
+
-- ---- RLS 注册与策略:声明在同目录 db/rls.json,由 `pxbaas app db-init` 应用 ----
|
|
32
|
+
-- anon 只读 published;authenticated 读 published 或本人;写/改/删限本人(author_id = jwt.sub)。
|
|
53
33
|
|
|
54
34
|
-- ---- 种子文章(3 篇 published,幂等)----
|
|
55
35
|
INSERT INTO `__SCHEMA__`.`posts_ssr` (title, content, author_id, author_name, status)
|
|
@@ -37,30 +37,45 @@ function esc(s: string): string {
|
|
|
37
37
|
.split(String.fromCharCode(34)).join(""");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
40
|
+
// Path contract (APP_WORKER_PATH_REWRITE off = current default): the proxy
|
|
41
|
+
// forwards /apps/{mount}/api/<x> to the worker as /<candidateSlug>/<x> — the
|
|
42
|
+
// candidate slug prefix is NOT stripped at runtime (the stripped path is only
|
|
43
|
+
// delivered via X-Pxbaas-Worker-Path when the rewrite kill-switch is ON, which
|
|
44
|
+
// is disabled by default due to the D18 streamRid regression). Normalize the
|
|
45
|
+
// slug prefix away and match on the logical path, so handlers resolve under ANY
|
|
46
|
+
// prefix shape (/{slug}/x, /api/x, or /x) and still work if the switch flips on.
|
|
47
|
+
// Same convention as the ssr-shop / ssr-blog / csr-bff templates.
|
|
48
|
+
function norm(path: string): string {
|
|
49
|
+
return path.replace(/^\/app_[^/]+/, "") || path;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
app.get("*", async (c) => {
|
|
53
|
+
const p = norm(new URL(c.req.url).pathname);
|
|
54
|
+
|
|
55
|
+
// L3 shape demo: server-side fetch of DB content via the derived internal base
|
|
56
|
+
// (anon key from env; RLS enforced server-side). Degrades gracefully so the
|
|
57
|
+
// shell still renders if the internal entry is ever unreachable.
|
|
58
|
+
if (p === "/ssr-data") {
|
|
59
|
+
let rows: Array<Record<string, unknown>> = [];
|
|
60
|
+
let note = "";
|
|
61
|
+
try {
|
|
62
|
+
const r = await fetch(internalBase() + "/rest/v1/?select=1", {
|
|
63
|
+
headers: { apikey: Deno.env.get("SUPABASE_ANON_KEY") || "" },
|
|
64
|
+
});
|
|
65
|
+
note = `internal base reachable: http-${r.status}`;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
note = `internal base unreachable: ${String(e).slice(0, 60)}`;
|
|
68
|
+
}
|
|
69
|
+
return c.html(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>L3 demo</title></head><body>
|
|
55
70
|
<div id="__ssr" data-ssr="v1"><h1>L3 full-SSR demo</h1><p>${esc(note)}</p><pre>${esc(JSON.stringify(rows))}</pre></div>
|
|
56
71
|
</body></html>`);
|
|
57
|
-
}
|
|
72
|
+
}
|
|
58
73
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
if (p === "/page") {
|
|
75
|
+
const theme = c.req.query("theme") === "dark" ? "dark" : "light";
|
|
76
|
+
const bg = theme === "dark" ? "#111" : "#fafafa";
|
|
77
|
+
const fg = theme === "dark" ? "#eee" : "#222";
|
|
78
|
+
const html = `<!DOCTYPE html><html><head><meta charset="utf-8">
|
|
64
79
|
<title>{{APP_NAME}} SSR</title>
|
|
65
80
|
<style>body{font-family:sans-serif;max-width:720px;margin:40px auto;background:${bg};color:${fg}}</style>
|
|
66
81
|
</head><body>
|
|
@@ -77,16 +92,20 @@ app.get("/page", (c) => {
|
|
|
77
92
|
})();
|
|
78
93
|
</script>
|
|
79
94
|
</body></html>`;
|
|
80
|
-
|
|
81
|
-
}
|
|
95
|
+
return c.html(html);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// JSON API. /apps/{m}/api/data and /api/health normalize to /data and /health
|
|
99
|
+
// after the slug strip; the /api-prefixed shapes are kept as a defensive
|
|
100
|
+
// fallback for deployments that deliver the logical path un-stripped.
|
|
101
|
+
if (p === "/data" || p === "/api/data") {
|
|
102
|
+
return c.json({ ok: true, engine: "hono", now: new Date().toISOString() });
|
|
103
|
+
}
|
|
104
|
+
if (p === "/health" || p === "/api/health") {
|
|
105
|
+
return c.json({ ok: true });
|
|
106
|
+
}
|
|
82
107
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// fallthrough delivers the FULL logical path (/page). Register both shapes so
|
|
86
|
-
// the worker behaves identically when the rewrite kill-switch is flipped back on.
|
|
87
|
-
app.get("/data", (c) => c.json({ ok: true, engine: "hono", now: new Date().toISOString() }));
|
|
88
|
-
app.get("/api/data", (c) => c.json({ ok: true, engine: "hono", now: new Date().toISOString() }));
|
|
89
|
-
app.get("/health", (c) => c.json({ ok: true }));
|
|
90
|
-
app.get("/api/health", (c) => c.json({ ok: true }));
|
|
108
|
+
return c.json({ ok: false, error: "not_found", path: p }, 404);
|
|
109
|
+
});
|
|
91
110
|
|
|
92
111
|
Deno.serve(app.fetch);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Declarative RLS for the shop webapp. Applied by `pxbaas app db-init` (single-tenant AND multi-tenant). table_schema is NEVER declared here: single-tenant binds it to --schema (default public); multi-tenant binds it server-side to the tenant db. using/check expressions may embed {{request.jwt.claim.*}} templates.",
|
|
3
|
+
"tables": [
|
|
4
|
+
{ "table": "products", "rlsEnabled": true, "rlsForced": false },
|
|
5
|
+
{ "table": "orders", "rlsEnabled": true, "rlsForced": false },
|
|
6
|
+
{ "table": "order_items", "rlsEnabled": true, "rlsForced": false }
|
|
7
|
+
],
|
|
8
|
+
"policies": [
|
|
9
|
+
{ "table": "products", "name": "products_select_anon", "role": "anon", "command": "SELECT", "using": "1=1", "check": null, "permissive": true },
|
|
10
|
+
{ "table": "products", "name": "products_select_auth", "role": "authenticated", "command": "SELECT", "using": "1=1", "check": null, "permissive": true },
|
|
11
|
+
{ "table": "orders", "name": "orders_select_auth", "role": "authenticated", "command": "SELECT", "using": "`buyer_email` = {{request.jwt.claim.email}} OR {{request.jwt.claim.email}} = 'admin@shop.demo'", "check": null, "permissive": true },
|
|
12
|
+
{ "table": "orders", "name": "orders_insert_auth", "role": "authenticated", "command": "INSERT", "using": null, "check": "`buyer_email` = {{request.jwt.claim.email}}", "permissive": true },
|
|
13
|
+
{ "table": "order_items", "name": "order_items_select_auth", "role": "authenticated", "command": "SELECT", "using": "`buyer_email` = {{request.jwt.claim.email}} OR {{request.jwt.claim.email}} = 'admin@shop.demo'", "check": null, "permissive": true },
|
|
14
|
+
{ "table": "order_items", "name": "order_items_insert_auth", "role": "authenticated", "command": "INSERT", "using": null, "check": "`buyer_email` = {{request.jwt.claim.email}}", "permissive": true }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -6,15 +6,16 @@
|
|
|
6
6
|
-- · 多租户实例:__SCHEMA__ = tenant_<tenant_id>(每租户一套库)
|
|
7
7
|
-- sed 's/__SCHEMA__/public/g' db/schema.sql
|
|
8
8
|
--
|
|
9
|
-
--
|
|
10
|
-
-- · 单租户:
|
|
11
|
-
-- · 多租户:
|
|
12
|
-
--
|
|
9
|
+
-- 执行通道(推荐):`pxbaas app db-init <name>` —— 单/多租户通用,自动:
|
|
10
|
+
-- · 单租户:__SCHEMA__ → public,经 /pg/query(全局 admin)执行;
|
|
11
|
+
-- · 多租户:__SCHEMA__ 去限定(连接已锁租户库),经 /api/tenant-ddl 用
|
|
12
|
+
-- per-tenant service_role key 执行;GRANT 仅本租户库,天然隔离。
|
|
13
|
+
-- 本文件只含 DDL + 种子;RLS 声明在同目录 db/rls.json,由 db-init 一并应用。
|
|
13
14
|
--
|
|
14
15
|
-- ⚠️ RLS 元数据的落点(写错静默失效,等价于关闭 RLS):
|
|
15
|
-
-- 永远写中心库 supabase._mysqlrest_rls_tables / _mysqlrest_rls_policies
|
|
16
|
-
--
|
|
17
|
-
--
|
|
16
|
+
-- 永远写中心库 supabase._mysqlrest_rls_tables / _mysqlrest_rls_policies。
|
|
17
|
+
-- db-init 会自动绑定 table_schema(单租户=--schema,多租户=服务端强绑租户库),
|
|
18
|
+
-- 无需也不要手写库名。租户库内同名 _mysqlrest_rls_* 表没有任何服务读取,勿写入。
|
|
18
19
|
-- =============================================================================
|
|
19
20
|
|
|
20
21
|
-- ---- 表结构 ----
|
|
@@ -54,41 +55,10 @@ CREATE TABLE IF NOT EXISTS `__SCHEMA__`.`order_items` (
|
|
|
54
55
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
55
56
|
);
|
|
56
57
|
|
|
57
|
-
-- ---- RLS
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-- ---- RLS 策略 ----
|
|
62
|
-
-- products:公开目录,anon / authenticated 均可读
|
|
63
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
64
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
65
|
-
VALUES ('__SCHEMA__','products','products_select_anon','anon','SELECT','1=1',NULL,1);
|
|
66
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
67
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
68
|
-
VALUES ('__SCHEMA__','products','products_select_auth','authenticated','SELECT','1=1',NULL,1);
|
|
69
|
-
|
|
70
|
-
-- orders:仅本人(buyer_email = jwt.email)或管理员白名单可读;仅本人可写
|
|
71
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
72
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
73
|
-
VALUES ('__SCHEMA__','orders','orders_select_auth','authenticated','SELECT',
|
|
74
|
-
'`buyer_email` = {{request.jwt.claim.email}} OR {{request.jwt.claim.email}} = ''admin@shop.demo''',
|
|
75
|
-
NULL, 1);
|
|
76
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
77
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
78
|
-
VALUES ('__SCHEMA__','orders','orders_insert_auth','authenticated','INSERT',NULL,
|
|
79
|
-
'`buyer_email` = {{request.jwt.claim.email}}', 1);
|
|
80
|
-
|
|
81
|
-
-- order_items:仅本人(buyer_email = jwt.email)或管理员白名单可读;仅本人可写(owner 门禁落在本表,
|
|
82
|
-
-- 不依赖前端按 order_id 过滤 —— 前端过滤不是安全边界,R2-05)。
|
|
83
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
84
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
85
|
-
VALUES ('__SCHEMA__','order_items','order_items_select_auth','authenticated','SELECT',
|
|
86
|
-
'`buyer_email` = {{request.jwt.claim.email}} OR {{request.jwt.claim.email}} = ''admin@shop.demo''',
|
|
87
|
-
NULL, 1);
|
|
88
|
-
INSERT IGNORE INTO supabase._mysqlrest_rls_policies
|
|
89
|
-
(table_schema, table_name, policy_name, role_name, command, using_expr, check_expr, permissive)
|
|
90
|
-
VALUES ('__SCHEMA__','order_items','order_items_insert_auth','authenticated','INSERT',NULL,
|
|
91
|
-
'`buyer_email` = {{request.jwt.claim.email}}', 1);
|
|
58
|
+
-- ---- RLS 注册与策略:声明在同目录 db/rls.json,由 `pxbaas app db-init` 应用 ----
|
|
59
|
+
-- · products:公开目录,anon / authenticated 均可读;
|
|
60
|
+
-- · orders / order_items:仅本人(buyer_email = jwt.email)或管理员白名单可读,仅本人可写。
|
|
61
|
+
-- (不再内联 INSERT,避免与 rls.json 双源;如需手动注册见 db/rls.json。)
|
|
92
62
|
|
|
93
63
|
-- ---- 种子商品(10 件,幂等)----
|
|
94
64
|
INSERT INTO `__SCHEMA__`.`products` (name, description, price_cents, stock, emoji, category)
|