@somewhere-cloud/cli 0.38.17 → 0.38.18
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/CHANGELOG.md +14 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/runtime/DECLARED-DATA-VENDOR.json +3 -3
- package/runtime/PLATFORM-SOURCE.json +1 -1
- package/runtime/SOURCE-LINT-VENDOR.json +7 -7
- package/runtime/declared-data.cjs +26 -4
- package/runtime/source-lint.cjs +65 -19
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.38.18
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- `somewhere typecheck` and `somewhere lint` use the platform's current
|
|
8
|
+
schema parser, `somewhere/db` types and lint rules (tech c077686c).
|
|
9
|
+
`sw.db.insert(table, values, { onConflict: 'ignore' | 'update' })` and
|
|
10
|
+
`sw.db.tx` insert intents with the same `options` now type on every table a
|
|
11
|
+
signed-in caller writes: `owner()`, `shared()`, `member()`, `group()`,
|
|
12
|
+
`parent()`, `anyOf(owner(), …)` including `grant()`, and `allOf()`.
|
|
13
|
+
`onConflict: 'update'` is a type error where the platform refuses it: a
|
|
14
|
+
table whose rule never grants update, a per-row `grant()` shares table, a
|
|
15
|
+
table named `excluded`, and an insert with `idempotencyKey`.
|
|
16
|
+
|
|
3
17
|
## 0.38.17
|
|
4
18
|
|
|
5
19
|
### Fixed
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@somewhere-cloud/cli",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.18",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@somewhere-cloud/cli",
|
|
9
|
-
"version": "0.38.
|
|
9
|
+
"version": "0.38.18",
|
|
10
10
|
"bundleDependencies": [
|
|
11
11
|
"@modelcontextprotocol/sdk",
|
|
12
12
|
"chokidar",
|
package/package.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"esbuild": "0.24.0",
|
|
3
3
|
"source_files": {
|
|
4
4
|
"worker/containers/compile/checker-contract.cjs": "3a8355b9477d89d00bca47cf74032c3b3c6458941cf5c57ddae9e2228b51915d",
|
|
5
|
-
"worker/containers/compile/declared-data-contract.cjs": "
|
|
5
|
+
"worker/containers/compile/declared-data-contract.cjs": "4c853d6a21def9d4443f3e8873d177dbbe4e957457e8293cd183830a81727565",
|
|
6
6
|
"worker/containers/compile/generated-declarations-render.cjs": "7ffdb34645cc9eccac6eba39dc86b646a41c4de8a1a038d2b5b46f42df9f1157",
|
|
7
7
|
"worker/containers/compile/generated-declarations.cjs": "6e9902e054c5c784fd9b3c3fafc1869e79954100a45014e1132247cd82833ec4",
|
|
8
8
|
"worker/containers/compile/groups-types.cjs": "7c22b4bb96c9391005d434852600dc02bc8a6bbb99d78d2b8e3eb152f7b72c58",
|
|
9
|
-
"worker/containers/compile/runtime-types.cjs": "
|
|
9
|
+
"worker/containers/compile/runtime-types.cjs": "fa9767ee2a0727511701821d0c18c878b7ce7b9ab3e92674057b474ad25e548e",
|
|
10
10
|
"worker/containers/compile/schema-types.cjs": "aa8987178dcea6bdee593e7ff06f452265b2af9d12d1bd1c47c983db15965a73",
|
|
11
11
|
"worker/containers/compile/typed-data.cjs": "1c8bec853fbee4ffd34c651076a4e1f4d8b18c0c2be5e99e31df7c9283207627",
|
|
12
12
|
"worker/containers/compile/typed-files.cjs": "a0b1818afbc7673f84583c76436a3b3ca1d1fdf63d8d8db138b36646103f0708",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"worker/src/utils/managed-groups/policy.ts": "2c5b48479a2925bea9a820022c63ca5fa827e2ff5531fe1ab6c0b058b7212b39",
|
|
37
37
|
"worker/src/utils/reference-check-inputs.ts": "59c7c8d3b4aaf8bffb2a54964e41f909437745d001b093edd74cc135f4edcf67"
|
|
38
38
|
},
|
|
39
|
-
"sha256": "
|
|
39
|
+
"sha256": "c62061ce7285f32fa53bf1cef84b3455bd13d0af05f2bbbc1af1b8abf9de9f09"
|
|
40
40
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"shared/d1-session.ts": "2ae29b74bba68016c86d25fa2a58f8c2fce4aca8fe57fafc253d380c47a3c0ab",
|
|
7
7
|
"shared/draft-execution-authority.ts": "607db6162732cc55798f1b871103c49b246f6667c1e4c7526abd85416a50abf6",
|
|
8
8
|
"shared/public-result.ts": "85ba1a6dd4136fd7362cf7f38b3042b1cacfc389af8c28343398c1ba83d26298",
|
|
9
|
-
"worker/containers/compile/declared-data-contract.cjs": "
|
|
9
|
+
"worker/containers/compile/declared-data-contract.cjs": "4c853d6a21def9d4443f3e8873d177dbbe4e957457e8293cd183830a81727565",
|
|
10
10
|
"worker/containers/compile/function-route.cjs": "e7faf1104ba95e27921901b4619bd98aa6a2be47cc10af00097fd4957744606b",
|
|
11
11
|
"worker/node_modules/acorn/dist/acorn.mjs": "b4c8c70200e72bae33cf1085e0ecb1e792c1b6924ed50cab817caf14f51bb249",
|
|
12
12
|
"worker/src/runtime/access-token.ts": "f6ab74f3a9e0bbfb15ac5a6a2f7391f4fb1e4e03b7358fbf8b61d962d14582ef",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"worker/src/runtime/crypto.ts": "5c91be24e7da79a8c11f04e06f70158ce89e09940a0918711c656d4cc893915d",
|
|
31
31
|
"worker/src/runtime/db-aggregate.ts": "e4c13540182e5990c49f5bd8e3ee53ca387ef2c87787ebc4daf8406d13e9375a",
|
|
32
32
|
"worker/src/runtime/db-coalesce.ts": "260fd91b0741f709c53005036528ac77669810296c4f82294bdef8cff6a36fdc",
|
|
33
|
-
"worker/src/runtime/db-composed-writes.ts": "
|
|
33
|
+
"worker/src/runtime/db-composed-writes.ts": "9c0fb6e8118cddf5dab51a2c5e2250a74097d4515349777d86365400943d0b7b",
|
|
34
34
|
"worker/src/runtime/db-generated-owner-read.ts": "875a1ed906272dc03b4fa9f73ecf9621e09b6c5e3b52905de70d0ae9861f8bec",
|
|
35
35
|
"worker/src/runtime/db-intake.ts": "d7b7f4b5865f002c741a06c8214af4e54806295826ab69575ed9c403100f0752",
|
|
36
36
|
"worker/src/runtime/db-live-invalidation.ts": "7697afe746de632cf8669dec657e340185e989ec87c76322ff0755f0b5d8b06f",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"worker/src/runtime/db-result-fidelity.ts": "224ae06727afcccd648ed73cffb4216321542e53d2bf7a7212dd0c5d00792f4e",
|
|
44
44
|
"worker/src/runtime/db-row-links.ts": "af41e679181cb4ecefb2df04f0d8f91f1f23abf50d0594c347fff15d59d4cb66",
|
|
45
45
|
"worker/src/runtime/db-session-recovery.ts": "7115703aad233f46295915273b05d04f4848ebe05ff1ec0f25f5ce5d298b418e",
|
|
46
|
-
"worker/src/runtime/db-structured-execution.ts": "
|
|
47
|
-
"worker/src/runtime/db-typed-errors.ts": "
|
|
46
|
+
"worker/src/runtime/db-structured-execution.ts": "c562aa1cbfdc894af5ea6d064f888b1b08eaa0fdd0a6a3ffdfd132ce78ed7083",
|
|
47
|
+
"worker/src/runtime/db-typed-errors.ts": "508f0cc79e85f6722c6696f777f4dfdf6e65ac8ff17157a8ae9b5358377c20b3",
|
|
48
48
|
"worker/src/runtime/db.ts": "87e7f7c6c6f4491ef36f85e6a23c9a612c09297ce1d0ffaabf281fca0d26728a",
|
|
49
49
|
"worker/src/runtime/declared-data-read-plan.ts": "e1b446eea9d274099e2151e280e115d692fc59e94f0f0c872ab9defcb1dfec5c",
|
|
50
50
|
"worker/src/runtime/declared-data.ts": "011cc2c747942bed9fd03b921cf2a5da91c881771821c78771e6ee78346b67f7",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"worker/src/runtime/postgres.ts": "168aafb45795dfec8796cc133e0f4d057cc9a917d846b4ca653b85e8eabf1498",
|
|
73
73
|
"worker/src/runtime/push-client.ts": "d8bf70c34336d22edeaeae9976ed3428fa18530bb4766718f8be439497dec0a4",
|
|
74
74
|
"worker/src/runtime/push.ts": "21b08bd7d3a3df2bd0c54fd393ed4ab2f898dab575d7b640320a8f4914c0932f",
|
|
75
|
-
"worker/src/runtime/query-sql.ts": "
|
|
75
|
+
"worker/src/runtime/query-sql.ts": "dd13ee3474992131795dae65a36f79d3e3540e8c23a71035b58d0c9558006edd",
|
|
76
76
|
"worker/src/runtime/queue.ts": "8f0bdda9bb857125d42cfbe0a4a50561daca56cc929f17bc91e1d4ba151cc998",
|
|
77
77
|
"worker/src/runtime/rate-limit.ts": "a63481a2144a46f39228b3e22b2c18df6e4f02d3ae4fac928cd2ee708cc1d1c8",
|
|
78
78
|
"worker/src/runtime/render.ts": "cbb484f18e410d26fcc87db259e1d10a1b18308405256b6c1a26afc2985707ed",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"worker/src/runtime/storage.ts": "15c7953b994497c7883b774f79ae93bae96e16373a0826cc733d21005de22ad7",
|
|
84
84
|
"worker/src/runtime/tasks.ts": "75efa2e3083853bfba1dc49745ede8865ddd218b84d568a00da2dbd669f9ab23",
|
|
85
85
|
"worker/src/runtime/verify-signup.ts": "dcac46b4581794fe875e3f85e3d93a2253b470f60a3c7fcee68543d8be8a6fa1",
|
|
86
|
-
"worker/src/runtime/version.ts": "
|
|
86
|
+
"worker/src/runtime/version.ts": "1ab816ccce86c94439957c91a1ea28f3860ce7c48cd52c6d5e6a403f12cdc70c",
|
|
87
87
|
"worker/src/runtime/visitor-session.generated.ts": "9753bc6c195a0072c21102d6caa853990ece23e80971b8726d311bdb93970b90",
|
|
88
88
|
"worker/src/runtime/web.ts": "269590d44ffa6239bb6e79999b58ba42435c230f045daea52bf73d09519afb0e",
|
|
89
89
|
"worker/src/security/browser-secret-scan.ts": "31ebc9dd94eed4d0a9a6b67b3b1e2505d4f7812c557c90337e034c1e3994c039",
|
|
@@ -144,6 +144,6 @@
|
|
|
144
144
|
"worker/src/utils/sql-tokens.ts": "ac643c765def38905a0600ae155ad909335cae1a7c1912da7bc87af25acb752e",
|
|
145
145
|
"worker/src/vendor/neondatabase-serverless/source.generated.ts": "96a3c18fde7d8888fdb53a4d7cd0721c238a234521d2084d1e66868a8df8e69c"
|
|
146
146
|
},
|
|
147
|
-
"sha256": "
|
|
147
|
+
"sha256": "0d11155b9a8bee2eab24f24c41a384ace071e994f6f925eebae2a4be16112cd2",
|
|
148
148
|
"fixtures_sha256": "908827d1430f4af5a83cae59ed476abc51dc9648757eac0089fe6e6db95f66c5"
|
|
149
149
|
}
|
|
@@ -684,7 +684,7 @@ ${generation >= 2 ? ROW_CAN_DECLARATION : ""}${receiptCapable ? localTypes ? INS
|
|
|
684
684
|
class DataError extends Error{constructor(status,payload){super(payload&&typeof payload.message==="string"?payload.message:payload&&typeof payload.error==="string"?payload.error:"Data operation failed");this.name="DataError";this.status=status;this.code=payload&&typeof payload.error==="string"?payload.error:null}}
|
|
685
685
|
const validateListLimit=input=>{if(input.limit!==undefined&&(!Number.isInteger(input.limit)||input.limit<1||input.limit>100))throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"limit must be an integer from 1 to 100"})};
|
|
686
686
|
let swReadSession=null,swInFlight=0,swStarts=0;const swReadSessionVoid=()=>{swReadSession=null;swStarts++};${generation >= 3 ? PUBLIC_BOOTSTRAP_CLIENT_RUNTIME : ""}const invoke=async(table,operation,input)=>{if(operation==="list"||operation==="relation_list")validateListLimit(input);${generation >= 3 ? PUBLIC_BOOTSTRAP_INVOKE : ""}const alone=swInFlight===0,start=++swStarts,isRead=operation==="list"||operation==="get"||operation==="aggregate"||operation==="relation_list";swInFlight++;let response;try{response=await fetch("/__sw/data",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json",...(swReadSession?{"X-Sw-Read-Session":swReadSession}:{})},body:JSON.stringify({...input,contract,table,operation})})}catch(error){if(!isRead)swReadSession=null;throw error}finally{swInFlight--}const next=response.headers&&typeof response.headers.get==="function"?response.headers.get("X-Sw-Read-Session"):null;if(!alone||swStarts!==start||swInFlight!==0)swReadSession=null;else if(next&&/^swrs1[.][A-Za-z0-9_-]+[.][A-Za-z0-9_-]+$/.test(next))swReadSession=next;else if(!isRead||!response.ok)swReadSession=null;const payload=await response.json().catch(()=>{throw new DataError(response.status,{error:"INVALID_DATA_RESPONSE",message:"Data operation returned an invalid response"})});if(!response.ok)throw new DataError(response.status,payload);return payload};
|
|
687
|
-
${receiptCapable ? INSERT_CLIENT_RUNTIME + "const receiptTables=" + JSON.stringify(receiptTables) + ";\n" : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_RUNTIME + CREATOR_CLIENT_RUNTIME : ""}${intakeTables.length ? INTAKE_CLIENT_RUNTIME : ""}${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"||operation==="aggregate"?(options={})=>invoke(table,operation,options):operation==="create"?${receiptCapable ? "(values,...extra)=>createWithReceipt(table,values,extra,receiptTables.includes(table))" : "values
|
|
687
|
+
${receiptCapable ? INSERT_CLIENT_RUNTIME + "const receiptTables=" + JSON.stringify(receiptTables) + ";\n" : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_RUNTIME + CREATOR_CLIENT_RUNTIME : ""}${intakeTables.length ? INTAKE_CLIENT_RUNTIME : ""}${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"||operation==="aggregate"?(options={})=>invoke(table,operation,options):operation==="create"?${receiptCapable ? "(values,...extra)=>createWithReceipt(table,values,extra,receiptTables.includes(table))" : '(values,...extra)=>{if(extra.length!==0)throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"create takes only the values to insert. Nothing was sent."});return invoke(table,operation,{values})}'}:${anonymousTables.length ? 'operation==="createAnonymous"?(values,...extra)=>createAnonymous(table,values,extra):' : ""}${intakeTables.length ? 'operation==="intake"?(reference,values,...extra)=>intake(table,reference,values,extra):' : ""}operation==="update"?(id,values)=>invoke(table,operation,{id,values}):operation==="createLink"?(id,options)=>createLink(table,id,options):operation==="links"?id=>invoke(table,"link_list",{id}):operation==="revokeLink"?(id,link_id)=>invoke(table,"link_revoke",{id,link_id}):${generation >= 2 ? 'operation==="get"?(id,options={})=>invoke(table,operation,options&&options.can===true?{id,can:true}:{id}):' : ""}id=>invoke(table,operation,{id})]);if(relations.length)entries.push(["relations",Object.freeze(Object.fromEntries(relations.map(relation=>[relation,Object.freeze({list:(parent_id,options={})=>invoke(table,"relation_list",{...options,relation,parent_id})})])))]);return[table,Object.freeze(Object.fromEntries(entries))];}).concat(${linkedSpec.length ? `[["links",links],["linked",linkedView(${JSON.stringify(linkedSpec)})]]` : "[]"})${creatorSpec.length ? `.concat([["creators",creators],["creator",creatorView(${JSON.stringify(creatorSpec)})]])` : ""}));
|
|
688
688
|
export{data,DataError};
|
|
689
689
|
`;
|
|
690
690
|
return { contract_digest, declaration, runtime, manifest: { version: 1, contract_digest, tables, declaration, ...insertReceiptCapability === void 0 ? {} : { insert_receipt: insertReceiptCapability } } };
|
|
@@ -963,6 +963,13 @@ interface SomewhereDbPlainInsertOptions { onConflict?: 'ignore' | 'update' | nul
|
|
|
963
963
|
// signed-in user or visitor on a table keyed by id() with no blob() column. Any other insert
|
|
964
964
|
// given a key refuses before anything runs. Eligible inserts without one get a fresh key.
|
|
965
965
|
interface SomewhereDbInsertOptions extends SomewhereDbPlainInsertOptions { idempotencyKey?: string }
|
|
966
|
+
// A caller upsert runs under the table's declared rule: 'update' also needs update on the conflicting
|
|
967
|
+
// row, so a declared table whose rule never grants update (or a per-row shares table) takes 'ignore'
|
|
968
|
+
// only. A key never combines with onConflict.
|
|
969
|
+
type SomewhereDbConflict<T extends string> = __SomewhereDeclared<T, 'conflict', 'ignore' | 'update'>;
|
|
970
|
+
type SomewhereDbCallerInsertOptions<T extends string> =
|
|
971
|
+
| { onConflict?: SomewhereDbConflict<T> | null; idempotencyKey?: never }
|
|
972
|
+
| { onConflict?: null; idempotencyKey?: string };
|
|
966
973
|
interface SomewhereDbReceipt { readonly replayed: boolean; readonly created_at: number; readonly result_until: number; readonly refuse_until: number }
|
|
967
974
|
// Process-local: retry() repeats this exact request with the same key under the current
|
|
968
975
|
// context's authority, until eight days after createdAt. It never runs by itself.
|
|
@@ -980,7 +987,7 @@ interface SomewhereDbRemove { where?: SomewhereDbWhere | null }
|
|
|
980
987
|
// (insert values, update set/increment). With no declared tables every name and
|
|
981
988
|
// value stays accepted, as before. Insert and Set pick caller or server identity.
|
|
982
989
|
type __SomewhereDbIntentFor<T extends string, Insert extends 'insert' | 'serverInsert', Set extends 'set' | 'serverSet'> =
|
|
983
|
-
| { op: 'insert'; table: T; values: __SomewhereDeclared<T, Insert, SomewhereDbValues>; options?: SomewhereDbPlainInsertOptions | null }
|
|
990
|
+
| { op: 'insert'; table: T; values: __SomewhereDeclared<T, Insert, SomewhereDbValues>; options?: (Insert extends 'insert' ? { onConflict?: SomewhereDbConflict<T> | null } : SomewhereDbPlainInsertOptions) | null }
|
|
984
991
|
| ({ op: 'update'; table: T } & SomewhereDbUpdateFor<T, Set>)
|
|
985
992
|
| { op: 'remove'; table: T; where?: SomewhereDbWhere | null };
|
|
986
993
|
// sw.db.server.tx: server authority may name row identity (owner column, shared authorship).
|
|
@@ -1066,7 +1073,7 @@ interface SomewhereServerDb {
|
|
|
1066
1073
|
}
|
|
1067
1074
|
interface SomewhereCallerDb extends Omit<SomewhereServerDb, 'from' | 'count' | 'insert' | 'update' | 'tx'> {
|
|
1068
1075
|
// The platform composes row identity from the verified user.
|
|
1069
|
-
insert<T extends SomewhereDbTable>(table: T, values: __SomewhereDeclared<T, 'insert', SomewhereDbValues>, options?:
|
|
1076
|
+
insert<T extends SomewhereDbTable>(table: T, values: __SomewhereDeclared<T, 'insert', SomewhereDbValues>, options?: SomewhereDbCallerInsertOptions<T> | null): Promise<SomewhereDbInsertResult<SomewhereDbRow<T>>>;
|
|
1070
1077
|
update<T extends SomewhereDbTable>(table: T, spec: SomewhereDbUpdateFor<T, 'set'>): Promise<SomewhereDbResult<SomewhereDbRow<T>>>;
|
|
1071
1078
|
tx(intents: readonly SomewhereDbCallerWriteIntent[]): Promise<SomewhereDbResult[]>;
|
|
1072
1079
|
from<T extends SomewhereDbTable, const C extends readonly SomewhereDbColumn<T>[] = readonly SomewhereDbColumn<T>[], const I extends readonly SomewhereDbRelationName<T>[] = []>(
|
|
@@ -3841,11 +3848,26 @@ interface SomewhereEndpointConfig<Auth extends SomewhereEndpointAuth, Schema ext
|
|
|
3841
3848
|
const find = (policy) => !policy ? null : policy.k === "o" ? policy.c : policy.k === "a" ? find(policy.p[0]) || find(policy.p[1]) : policy.k === "n" ? find(policy.w) : null;
|
|
3842
3849
|
return find(table.policy);
|
|
3843
3850
|
}
|
|
3851
|
+
function declaredRuleGrants(policy, op) {
|
|
3852
|
+
if (!policy) return true;
|
|
3853
|
+
if (policy.k === "o") return true;
|
|
3854
|
+
if (policy.k === "m") return !policy.m.o || policy.m.o.includes(op);
|
|
3855
|
+
if (policy.k === "a") return declaredRuleGrants(policy.p[0], op) || declaredRuleGrants(policy.p[1], op);
|
|
3856
|
+
if (policy.k === "n") return declaredRuleGrants(policy.g, op) && declaredRuleGrants(policy.w, op) && (op !== "update" || declaredRuleGrants(policy.g, "read") && declaredRuleGrants(policy.w, "read"));
|
|
3857
|
+
if (policy.k === "g" || policy.k === "r") return !!(policy.roles && policy.roles[op] && policy.roles[op].length);
|
|
3858
|
+
if (policy.k === "p") return declaredRuleGrants(policy.p, op);
|
|
3859
|
+
return true;
|
|
3860
|
+
}
|
|
3844
3861
|
function declaredTablesDeclaration3(authority) {
|
|
3845
3862
|
const schema = authority && authority.schema || {};
|
|
3846
3863
|
const scopes = authority && authority.scopes || {};
|
|
3847
3864
|
const names = Object.keys(schema).sort();
|
|
3848
3865
|
if (!names.length) return "";
|
|
3866
|
+
const rowShares = /* @__PURE__ */ new Set();
|
|
3867
|
+
for (const name of names) {
|
|
3868
|
+
const policy = schema[name].policy, branch = policy && policy.k === "a" ? policy.p[1] : null;
|
|
3869
|
+
if (branch && branch.k === "m" && branch.m.og && branch.m.g.length === 2) rowShares.add(String(branch.m.m).toLowerCase());
|
|
3870
|
+
}
|
|
3849
3871
|
const key = JSON.stringify;
|
|
3850
3872
|
const shape = (fields) => fields.length ? "{ " + fields.join("; ") + " }" : "Record<string, never>";
|
|
3851
3873
|
const union = (columns) => columns.length ? columns.map((column) => key(column.n)).join(" | ") : "never";
|
|
@@ -3863,7 +3885,7 @@ interface SomewhereEndpointConfig<Auth extends SomewhereEndpointAuth, Schema ext
|
|
|
3863
3885
|
if (owner && !columns.some((column) => column.n === owner)) columns.push({ n: owner, t: "text", optional: false, identity: true, type: "string" });
|
|
3864
3886
|
const declared = columns.filter((column) => !column.identity);
|
|
3865
3887
|
const relations = (table.relations || []).filter((relation) => Object.prototype.hasOwnProperty.call(schema, relation.table)).map((relation) => key(relation.name) + ": SomewhereDeclaredTables[" + key(relation.table) + "]['row']" + (relation.kind === "belongsTo" ? " | null" : "[]"));
|
|
3866
|
-
return " " + key(name) + ": {\n row: " + shape(columns.map((column) => field(column, false))) + ";\n insert: " + shape(declared.map((column) => field(column, column.optional))) + ";\n serverInsert: " + shape(columns.map((column) => field(column, column.optional))) + ";\n set: " + shape(declared.map((column) => field(column, true))) + ";\n serverSet: " + shape(columns.map((column) => field(column, true))) + ";\n numeric: " + union(declared.filter((column) => column.t === "integer" || column.t === "number")) + ";\n scalar: " + union(columns.filter((column) => (!column.identity || column.n === owner) && column.t !== "json" && column.t !== "blob")) + ";\n relations: " + (relations.length ? shape(relations) : "{}") + ";\n };\n";
|
|
3888
|
+
return " " + key(name) + ": {\n row: " + shape(columns.map((column) => field(column, false))) + ";\n insert: " + shape(declared.map((column) => field(column, column.optional))) + ";\n serverInsert: " + shape(columns.map((column) => field(column, column.optional))) + ";\n set: " + shape(declared.map((column) => field(column, true))) + ";\n serverSet: " + shape(columns.map((column) => field(column, true))) + ";\n numeric: " + union(declared.filter((column) => column.t === "integer" || column.t === "number")) + ";\n scalar: " + union(columns.filter((column) => (!column.identity || column.n === owner) && column.t !== "json" && column.t !== "blob")) + ";\n relations: " + (relations.length ? shape(relations) : "{}") + ";\n conflict: " + (rowShares.has(name.toLowerCase()) || !declaredRuleGrants(table.policy || (table.member ? { k: "m", m: table.member } : null), "update") ? "'ignore'" : "'ignore' | 'update'") + ";\n };\n";
|
|
3867
3889
|
});
|
|
3868
3890
|
return "interface SomewhereDeclaredTables {\n" + entries.join("") + "}\n";
|
|
3869
3891
|
}
|
package/runtime/source-lint.cjs
CHANGED
|
@@ -729,7 +729,7 @@ ${generation >= 2 ? ROW_CAN_DECLARATION : ""}${receiptCapable ? localTypes ? INS
|
|
|
729
729
|
class DataError extends Error{constructor(status,payload){super(payload&&typeof payload.message==="string"?payload.message:payload&&typeof payload.error==="string"?payload.error:"Data operation failed");this.name="DataError";this.status=status;this.code=payload&&typeof payload.error==="string"?payload.error:null}}
|
|
730
730
|
const validateListLimit=input=>{if(input.limit!==undefined&&(!Number.isInteger(input.limit)||input.limit<1||input.limit>100))throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"limit must be an integer from 1 to 100"})};
|
|
731
731
|
let swReadSession=null,swInFlight=0,swStarts=0;const swReadSessionVoid=()=>{swReadSession=null;swStarts++};${generation >= 3 ? PUBLIC_BOOTSTRAP_CLIENT_RUNTIME : ""}const invoke=async(table,operation,input)=>{if(operation==="list"||operation==="relation_list")validateListLimit(input);${generation >= 3 ? PUBLIC_BOOTSTRAP_INVOKE : ""}const alone=swInFlight===0,start=++swStarts,isRead=operation==="list"||operation==="get"||operation==="aggregate"||operation==="relation_list";swInFlight++;let response;try{response=await fetch("/__sw/data",{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/json",...(swReadSession?{"X-Sw-Read-Session":swReadSession}:{})},body:JSON.stringify({...input,contract,table,operation})})}catch(error){if(!isRead)swReadSession=null;throw error}finally{swInFlight--}const next=response.headers&&typeof response.headers.get==="function"?response.headers.get("X-Sw-Read-Session"):null;if(!alone||swStarts!==start||swInFlight!==0)swReadSession=null;else if(next&&/^swrs1[.][A-Za-z0-9_-]+[.][A-Za-z0-9_-]+$/.test(next))swReadSession=next;else if(!isRead||!response.ok)swReadSession=null;const payload=await response.json().catch(()=>{throw new DataError(response.status,{error:"INVALID_DATA_RESPONSE",message:"Data operation returned an invalid response"})});if(!response.ok)throw new DataError(response.status,payload);return payload};
|
|
732
|
-
${receiptCapable ? INSERT_CLIENT_RUNTIME + "const receiptTables=" + JSON.stringify(receiptTables) + ";\n" : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_RUNTIME + CREATOR_CLIENT_RUNTIME : ""}${intakeTables.length ? INTAKE_CLIENT_RUNTIME : ""}${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"||operation==="aggregate"?(options={})=>invoke(table,operation,options):operation==="create"?${receiptCapable ? "(values,...extra)=>createWithReceipt(table,values,extra,receiptTables.includes(table))" : "values
|
|
732
|
+
${receiptCapable ? INSERT_CLIENT_RUNTIME + "const receiptTables=" + JSON.stringify(receiptTables) + ";\n" : ""}${anonymousTables.length ? ANONYMOUS_CLIENT_RUNTIME + CREATOR_CLIENT_RUNTIME : ""}${intakeTables.length ? INTAKE_CLIENT_RUNTIME : ""}${linkedSpec.length ? LINK_CLIENT_RUNTIME : ""}const data=Object.freeze(Object.fromEntries(${JSON.stringify(runtimeTables)}.map(([table,operations,relations])=>{const entries=operations.map(operation=>[operation,operation==="list"||operation==="aggregate"?(options={})=>invoke(table,operation,options):operation==="create"?${receiptCapable ? "(values,...extra)=>createWithReceipt(table,values,extra,receiptTables.includes(table))" : '(values,...extra)=>{if(extra.length!==0)throw new DataError(400,{error:"DATA_INPUT_INVALID",message:"create takes only the values to insert. Nothing was sent."});return invoke(table,operation,{values})}'}:${anonymousTables.length ? 'operation==="createAnonymous"?(values,...extra)=>createAnonymous(table,values,extra):' : ""}${intakeTables.length ? 'operation==="intake"?(reference,values,...extra)=>intake(table,reference,values,extra):' : ""}operation==="update"?(id,values)=>invoke(table,operation,{id,values}):operation==="createLink"?(id,options)=>createLink(table,id,options):operation==="links"?id=>invoke(table,"link_list",{id}):operation==="revokeLink"?(id,link_id)=>invoke(table,"link_revoke",{id,link_id}):${generation >= 2 ? 'operation==="get"?(id,options={})=>invoke(table,operation,options&&options.can===true?{id,can:true}:{id}):' : ""}id=>invoke(table,operation,{id})]);if(relations.length)entries.push(["relations",Object.freeze(Object.fromEntries(relations.map(relation=>[relation,Object.freeze({list:(parent_id,options={})=>invoke(table,"relation_list",{...options,relation,parent_id})})])))]);return[table,Object.freeze(Object.fromEntries(entries))];}).concat(${linkedSpec.length ? `[["links",links],["linked",linkedView(${JSON.stringify(linkedSpec)})]]` : "[]"})${creatorSpec.length ? `.concat([["creators",creators],["creator",creatorView(${JSON.stringify(creatorSpec)})]])` : ""}));
|
|
733
733
|
export{data,DataError};
|
|
734
734
|
`;
|
|
735
735
|
return { contract_digest, declaration, runtime, manifest: { version: 1, contract_digest, tables, declaration, ...insertReceiptCapability === void 0 ? {} : { insert_receipt: insertReceiptCapability } } };
|
|
@@ -1547,6 +1547,8 @@ var DB_TYPED_ERRORS_JS = ` // The REST bridge's refusal as the caller sees
|
|
|
1547
1547
|
// move only a grant() would allow.
|
|
1548
1548
|
const refused = new Error(label !== 'sw.db.tx' && guard[1] === 'POLICY_AUTHORITY_REQUIRED'
|
|
1549
1549
|
? label + ': the access policy of this table does not permit this change by the signed-in user. Nothing was written.'
|
|
1550
|
+
: label !== 'sw.db.tx'
|
|
1551
|
+
? label + ': this row targets a group the signed-in user is not a member of. Nothing was written.'
|
|
1550
1552
|
: guard[1] === 'MEMBERSHIP_REQUIRED'
|
|
1551
1553
|
? 'sw.db.tx: an insert targets a group the signed-in user is not a member of. Nothing in the transaction was written.'
|
|
1552
1554
|
: 'sw.db.tx: the access policy of a table does not permit one of these changes by the signed-in user. Nothing in the transaction was written.');
|
|
@@ -4773,7 +4775,8 @@ var DB_STRUCTURED_EXECUTION_JS = ` async function __sw_structExec(label, ir
|
|
|
4773
4775
|
? 'sw.db.server.insert on "' + ir.table.name + '": a share can name only a "' + ir.scope.grantRow.t + '" row its user_id owns.'
|
|
4774
4776
|
: 'sw.db.insert on "' + ir.table.name + '": a share can name only a "' + ir.scope.grantRow.t + '" row the signed-in user owns.');
|
|
4775
4777
|
}
|
|
4776
|
-
|
|
4778
|
+
// An upsert's refusals raise inside the statement (query-sql.ts); its empty result is an ignored conflict.
|
|
4779
|
+
if (ir.kind === 'insert' && (ir.scope.mode === 'member' || ir.scope.mode === 'policy') && ir.onConflict === undefined && rows.length === 0) {
|
|
4777
4780
|
__sw_structThrow403(ir.scope.mode === 'policy' ? 'POLICY_AUTHORITY_REQUIRED' : 'MEMBERSHIP_REQUIRED',
|
|
4778
4781
|
ir.scope.mode === 'policy' ? 'sw.db.insert on "' + ir.table.name + '": the proposed row is outside your declared private policy.'
|
|
4779
4782
|
: 'sw.db.insert on "' + ir.table.name + '": you can only add rows to a group you belong to, and the signed-in user is not a member of the group named in this row. Add the user to the group first, or write to a group they belong to.');
|
|
@@ -5807,6 +5810,44 @@ function renderQuerySql(ir, principalId, returning, context) {
|
|
|
5807
5810
|
policyInsertPredicate(policy, proposedValues) {
|
|
5808
5811
|
return helpers.policySql(context.renderPolicy({ render: "insert", principal: principalId, table: ir.table.name, physical: context.physicalMap(), policy, proposed: proposedValues }));
|
|
5809
5812
|
},
|
|
5813
|
+
// The columns that place a row under its rule and never change through an upsert: member()
|
|
5814
|
+
// group keys, a group() column, an allOf gate key, the parent() link and a per-row grant's
|
|
5815
|
+
// row key. The owner is stamped and never assigned, so it is not listed.
|
|
5816
|
+
policyFixedKeys(policy) {
|
|
5817
|
+
const keys = [];
|
|
5818
|
+
if (policy.k === "m") {
|
|
5819
|
+
if (!policy.m.og) for (let i = 0; i < policy.m.g.length; i++) keys.push(policy.m.g[i]);
|
|
5820
|
+
else if (policy.m.g.length === 2) keys.push(policy.m.g[1]);
|
|
5821
|
+
}
|
|
5822
|
+
if (policy.k === "g") keys.push(policy.c);
|
|
5823
|
+
if (policy.k === "p") keys.push(policy.v);
|
|
5824
|
+
if (policy.k === "a" || policy.k === "n") {
|
|
5825
|
+
const parts = policy.k === "a" ? policy.p : [policy.g, policy.w];
|
|
5826
|
+
for (let i = 0; i < parts.length; i++) {
|
|
5827
|
+
const inner = helpers.policyFixedKeys(parts[i]);
|
|
5828
|
+
for (let j = 0; j < inner.length; j++) keys.push(inner[j]);
|
|
5829
|
+
}
|
|
5830
|
+
}
|
|
5831
|
+
return keys;
|
|
5832
|
+
},
|
|
5833
|
+
// Upsert under a private rule (tsk_c98959e4): the conflicting existing row must pass the rule's
|
|
5834
|
+
// UPDATE predicate (rendered by the caller, binds already pushed) and keep every fixed key the
|
|
5835
|
+
// caller names. Otherwise the statement raises, so it is refused whole (a transaction rolls back),
|
|
5836
|
+
// never skipped.
|
|
5837
|
+
conflictUpdateSql(updatePredicate, fixed) {
|
|
5838
|
+
const sets = context.map(ir.values, function(e) {
|
|
5839
|
+
return q(e.column) + " = excluded." + q(e.column);
|
|
5840
|
+
});
|
|
5841
|
+
if (sets.length === 0) context.fail('sw.db.insert with { onConflict: "update" } needs at least one column value to update on conflict.');
|
|
5842
|
+
const held = ["(" + updatePredicate + ")"];
|
|
5843
|
+
for (let i = 0; i < fixed.length; i++) {
|
|
5844
|
+
for (let j = 0; j < ir.values.length; j++) {
|
|
5845
|
+
const column = ir.values[j].column;
|
|
5846
|
+
if (context.lower(column) === context.lower(fixed[i])) held.push(tableSql + "." + q(column) + " IS excluded." + q(column));
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5849
|
+
return " ON CONFLICT DO UPDATE SET " + sets.join(", ") + " WHERE CASE WHEN " + held.join(" AND ") + " THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END";
|
|
5850
|
+
},
|
|
5810
5851
|
// Row-link authority (tsk_f1529568): ONE correlated EXISTS proving, in the
|
|
5811
5852
|
// statement itself, that the session token is live, belongs to this exact
|
|
5812
5853
|
// grant, the grant is unrevoked and unexpired, names this table and THIS
|
|
@@ -5964,6 +6005,8 @@ function renderQuerySql(ir, principalId, returning, context) {
|
|
|
5964
6005
|
};
|
|
5965
6006
|
const q = context.quote;
|
|
5966
6007
|
const tableSql = helpers.table(ir.table.name);
|
|
6008
|
+
if (ir.kind === "insert" && ir.onConflict === "update" && context.lower(tableSql) === '"excluded"')
|
|
6009
|
+
context.fail('sw.db.insert: a table named "excluded" cannot take onConflict: "update". Rename the table, or use onConflict: "ignore".');
|
|
5967
6010
|
const policyOperation = ir.kind === "select" || ir.kind === "count" || ir.kind === "aggregate" ? "read" : ir.kind === "insert" ? "create" : ir.kind;
|
|
5968
6011
|
let sql;
|
|
5969
6012
|
if (ir.kind === "select") {
|
|
@@ -6092,8 +6135,9 @@ function renderQuerySql(ir, principalId, returning, context) {
|
|
|
6092
6135
|
existsSql += " AND " + q(m.mg[i]) + " = ?";
|
|
6093
6136
|
params.push(gv);
|
|
6094
6137
|
}
|
|
6095
|
-
const guard = ir.txGuard ? "CASE WHEN EXISTS (" + existsSql + ") THEN 1 ELSE json_extract('{}', 'SW_MEMBERSHIP_REQUIRED') END" : "EXISTS (" + existsSql + ")";
|
|
6096
|
-
|
|
6138
|
+
const guard = ir.txGuard || ir.onConflict !== void 0 ? "CASE WHEN EXISTS (" + existsSql + ") THEN 1 ELSE json_extract('{}', 'SW_MEMBERSHIP_REQUIRED') END" : "EXISTS (" + existsSql + ")";
|
|
6139
|
+
const conflict = ir.onConflict === "ignore" ? " ON CONFLICT DO NOTHING" : ir.onConflict === "update" ? helpers.conflictUpdateSql(helpers.memberScopePredicate(m, null, "update"), m.g) : "";
|
|
6140
|
+
sql = "INSERT INTO " + tableSql + " (" + colList + ") SELECT " + placeholders + " WHERE " + guard + conflict + context.returningSql(returning);
|
|
6097
6141
|
return { sql, params };
|
|
6098
6142
|
}
|
|
6099
6143
|
if (ir.scope.mode === "link") {
|
|
@@ -6124,8 +6168,9 @@ function renderQuerySql(ir, principalId, returning, context) {
|
|
|
6124
6168
|
for (let i = 0; i < ivals.length; i++)
|
|
6125
6169
|
params.push(ivals[i]);
|
|
6126
6170
|
const authorized = helpers.policyInsertPredicate(ir.scope.policy, ir.values || []);
|
|
6127
|
-
const guard = ir.txGuard ? "CASE WHEN " + authorized + " THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END" : authorized;
|
|
6128
|
-
|
|
6171
|
+
const guard = ir.txGuard || ir.onConflict !== void 0 ? "CASE WHEN " + authorized + " THEN 1 ELSE json_extract('{}', 'SW_POLICY_AUTHORITY_REQUIRED') END" : authorized;
|
|
6172
|
+
const conflict = ir.onConflict === "ignore" ? " ON CONFLICT DO NOTHING" : ir.onConflict === "update" ? helpers.conflictUpdateSql(helpers.policyPredicate(ir.scope.policy, null, null, "update"), helpers.policyFixedKeys(ir.scope.policy)) : "";
|
|
6173
|
+
sql = "INSERT INTO " + tableSql + " (" + colList + ") SELECT " + placeholders + " WHERE " + guard + conflict + context.returningSql(returning);
|
|
6129
6174
|
return { sql, params };
|
|
6130
6175
|
}
|
|
6131
6176
|
if ((ir.scope.mode === "user" || ir.scope.mode === "server") && ir.scope.grantRow) {
|
|
@@ -6167,13 +6212,14 @@ function renderQuerySql(ir, principalId, returning, context) {
|
|
|
6167
6212
|
if (ir.onConflict === "ignore") {
|
|
6168
6213
|
sql += " ON CONFLICT DO NOTHING";
|
|
6169
6214
|
} else if (ir.onConflict === "update") {
|
|
6170
|
-
const sets = context.map(ir.values, function(e) {
|
|
6171
|
-
return q(e.column) + " = excluded." + q(e.column);
|
|
6172
|
-
});
|
|
6173
|
-
sql += " ON CONFLICT DO UPDATE SET " + sets.join(", ");
|
|
6174
6215
|
if (ir.scope.mode === "user") {
|
|
6175
|
-
sql += " WHERE " + q(ir.scope.ownerColumn) + " = ?";
|
|
6176
6216
|
params.push(principalId);
|
|
6217
|
+
sql += helpers.conflictUpdateSql(q(ir.scope.ownerColumn) + " = ?", []);
|
|
6218
|
+
} else {
|
|
6219
|
+
const sets = context.map(ir.values, function(e) {
|
|
6220
|
+
return q(e.column) + " = excluded." + q(e.column);
|
|
6221
|
+
});
|
|
6222
|
+
sql += " ON CONFLICT DO UPDATE SET " + sets.join(", ");
|
|
6177
6223
|
}
|
|
6178
6224
|
}
|
|
6179
6225
|
sql += context.returningSql(returning);
|
|
@@ -9759,14 +9805,14 @@ var DB_COMPOSED_WRITES_JS = ` // Per-column conditions are ANDed. A bounded
|
|
|
9759
9805
|
if (decided.scope.mode === 'user') __sw_structAssertNotOwnerColumn(cols, decided.scope.ownerColumn, 'insert');
|
|
9760
9806
|
if (decided.scope.mode === 'policy') __sw_structAssertPolicyWrite(cols, decided.scope.policy, 'insert');
|
|
9761
9807
|
if (decided.scope.mode === 'policy') __sw_structAssertPolicyInsert(cols, decided.scope.policy);
|
|
9762
|
-
if (decided.scope.mode === '
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
if (decided.scope.mode === '
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
|
|
9769
|
-
|
|
9808
|
+
if (decided.scope.mode === 'member') __sw_structAssertMemberGroupPresent(cols, decided.scope.member, 'insert');
|
|
9809
|
+
// An upsert under a rule needs create (checked above) and, for "update", update on the
|
|
9810
|
+
// conflicting row; a rule that never grants update refuses before anything runs.
|
|
9811
|
+
if (onConflict === 'update' && decided.scope.mode === 'policy' && !__sw_structPolicyAllows(decided.scope.policy, 'update')) {
|
|
9812
|
+
__sw_structThrow403('POLICY_OPERATION_NOT_ALLOWED', 'sw.db.insert on "' + table + '": onConflict: "update" updates the existing row, and the declared private policy does not grant update. Use onConflict: "ignore", or grant update.');
|
|
9813
|
+
}
|
|
9814
|
+
if (onConflict === 'update' && decided.scope.mode === 'member' && !__sw_structMemberAllows(decided.scope.member, 'update')) {
|
|
9815
|
+
__sw_structThrow403('MEMBER_OPERATION_NOT_ALLOWED', 'sw.db.insert on "' + table + '": onConflict: "update" updates the existing row, and member() does not grant update. Use onConflict: "ignore", or grant update.');
|
|
9770
9816
|
}
|
|
9771
9817
|
if (cols.length === 0 && decided.scope.mode !== 'user'
|
|
9772
9818
|
&& !(decided.scope.mode === 'policy' && __sw_structPolicyHasOwner(decided.scope.policy))) __sw_structThrow('sw.db.insert requires at least one column value.');
|