@pyric/cli 0.1.0-alpha.25 → 0.1.0-alpha.26
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/dist/rtdb/inspect-deployed-rules.d.ts +3 -17
- package/dist/rtdb/inspect-deployed-rules.d.ts.map +1 -1
- package/dist/rtdb/inspect-deployed-rules.js +4 -54
- package/dist/rtdb/inspect-deployed-rules.js.map +1 -1
- package/dist/serve/site-ui/docs/index.json +35 -0
- package/dist/serve/site-ui/docs/pyric-rules-reference-api/index.html +372 -201
- package/dist/serve/site-ui/docs/pyric-rules-reference-api.md +148 -9
- package/dist/serve/site-ui/docs/secure/rtdb-rules-in-typescript/index.html +56 -1
- package/dist/serve/site-ui/docs/secure/rtdb-rules-in-typescript.md +33 -0
- package/dist/serve/site-ui/examples/chess/index.html +1 -1
- package/package.json +4 -4
- package/src/rtdb/inspect-deployed-rules.ts +5 -77
|
@@ -2,6 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
## Classes
|
|
4
4
|
|
|
5
|
+
<a id="rtdbexpressionsyntaxerror"></a>
|
|
6
|
+
|
|
7
|
+
### RtdbExpressionSyntaxError
|
|
8
|
+
|
|
9
|
+
Why [canonicalRtdbExpression](#canonicalrtdbexpression) could not read its input.
|
|
10
|
+
|
|
11
|
+
#### Extends
|
|
12
|
+
|
|
13
|
+
- `Error`
|
|
14
|
+
|
|
15
|
+
#### Constructors
|
|
16
|
+
|
|
17
|
+
<a id="constructor"></a>
|
|
18
|
+
|
|
19
|
+
##### Constructor
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
new RtdbExpressionSyntaxError(raw: string, message: string): RtdbExpressionSyntaxError;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
###### Parameters
|
|
26
|
+
|
|
27
|
+
| Parameter | Type |
|
|
28
|
+
| :------ | :------ |
|
|
29
|
+
| `raw` | `string` |
|
|
30
|
+
| `message` | `string` |
|
|
31
|
+
|
|
32
|
+
###### Returns
|
|
33
|
+
|
|
34
|
+
[`RtdbExpressionSyntaxError`](#rtdbexpressionsyntaxerror)
|
|
35
|
+
|
|
36
|
+
###### Overrides
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
Error.constructor
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
#### Properties
|
|
43
|
+
|
|
44
|
+
| Property | Modifier | Type |
|
|
45
|
+
| :------ | :------ | :------ |
|
|
46
|
+
| <a id="raw"></a> `raw` | `readonly` | `string` |
|
|
47
|
+
|
|
48
|
+
***
|
|
49
|
+
|
|
5
50
|
<a id="rulesassertionerror"></a>
|
|
6
51
|
|
|
7
52
|
### RulesAssertionError
|
|
@@ -17,7 +62,7 @@ without extra wiring.
|
|
|
17
62
|
|
|
18
63
|
#### Constructors
|
|
19
64
|
|
|
20
|
-
<a id="constructor"></a>
|
|
65
|
+
<a id="constructor-1"></a>
|
|
21
66
|
|
|
22
67
|
##### Constructor
|
|
23
68
|
|
|
@@ -57,7 +102,7 @@ caller can surface them without re-parsing.
|
|
|
57
102
|
|
|
58
103
|
#### Constructors
|
|
59
104
|
|
|
60
|
-
<a id="constructor-
|
|
105
|
+
<a id="constructor-2"></a>
|
|
61
106
|
|
|
62
107
|
##### Constructor
|
|
63
108
|
|
|
@@ -106,7 +151,7 @@ fail on a known simulator gap.
|
|
|
106
151
|
|
|
107
152
|
#### Constructors
|
|
108
153
|
|
|
109
|
-
<a id="constructor-
|
|
154
|
+
<a id="constructor-3"></a>
|
|
110
155
|
|
|
111
156
|
##### Constructor
|
|
112
157
|
|
|
@@ -158,6 +203,18 @@ The outcome of running one Firestore case through `simulate`. Never a
|
|
|
158
203
|
|
|
159
204
|
***
|
|
160
205
|
|
|
206
|
+
<a id="diffrtdbrulesoptions"></a>
|
|
207
|
+
|
|
208
|
+
### DiffRtdbRulesOptions
|
|
209
|
+
|
|
210
|
+
#### Properties
|
|
211
|
+
|
|
212
|
+
| Property | Type | Description |
|
|
213
|
+
| :------ | :------ | :------ |
|
|
214
|
+
| <a id="ignoreequivalent"></a> `ignoreEquivalent?` | `boolean` | Treat two `.read`, `.write` or `.validate` expressions as the same rule when their [canonicalRtdbExpression](#canonicalrtdbexpression) texts are equal: spellings that cannot change any decision, such as `===` for `==` or `child('a/' + $id)` for `child('a').child($id)`. A changed entry still reports both expressions as written. An expression that does not parse is compared as written. Default `false`: expressions compare as text. |
|
|
215
|
+
|
|
216
|
+
***
|
|
217
|
+
|
|
161
218
|
<a id="evaluatedruleinfo"></a>
|
|
162
219
|
|
|
163
220
|
### EvaluatedRuleInfo
|
|
@@ -664,6 +721,26 @@ a file system.
|
|
|
664
721
|
|
|
665
722
|
***
|
|
666
723
|
|
|
724
|
+
<a id="rtdbrulechange"></a>
|
|
725
|
+
|
|
726
|
+
### RtdbRuleChange
|
|
727
|
+
|
|
728
|
+
One rule expression that differs. `deployed` is absent for `added` and
|
|
729
|
+
`local` is absent for `removed`: the change is what deploying the local
|
|
730
|
+
ruleset would do to the deployed one.
|
|
731
|
+
|
|
732
|
+
#### Properties
|
|
733
|
+
|
|
734
|
+
| Property | Type |
|
|
735
|
+
| :------ | :------ |
|
|
736
|
+
| <a id="change"></a> `change` | `"added"` \| `"removed"` \| `"changed"` |
|
|
737
|
+
| <a id="deployed"></a> `deployed?` | `string` |
|
|
738
|
+
| <a id="kind-3"></a> `kind` | [`RtdbRuleKind`](#rtdbrulekind) |
|
|
739
|
+
| <a id="local"></a> `local?` | `string` |
|
|
740
|
+
| <a id="path-2"></a> `path` | `string` |
|
|
741
|
+
|
|
742
|
+
***
|
|
743
|
+
|
|
667
744
|
<a id="rtdbrulecoverage"></a>
|
|
668
745
|
|
|
669
746
|
### RtdbRuleCoverage
|
|
@@ -678,9 +755,9 @@ a file system.
|
|
|
678
755
|
| <a id="evaluated"></a> `evaluated` | `number` | Evaluations that produced a verdict: `allow + deny + error`. |
|
|
679
756
|
| <a id="expression-1"></a> `expression` | `string` | The rule expression, or the declared index list for `indexOn`. |
|
|
680
757
|
| <a id="file-3"></a> `file?` | `string` | The rules file holding the node, when the summary covers several files. |
|
|
681
|
-
| <a id="kind-
|
|
758
|
+
| <a id="kind-4"></a> `kind` | [`RtdbCoverageKind`](#rtdbcoveragekind) | - |
|
|
682
759
|
| <a id="line-2"></a> `line?` | `number` | 1-indexed line in the rules file, when a line lookup was supplied. |
|
|
683
|
-
| <a id="path-
|
|
760
|
+
| <a id="path-3"></a> `path` | `string` | The rule node's path, such as `/rooms/$roomId`. |
|
|
684
761
|
| <a id="status"></a> `status` | [`RtdbCoverageStatus`](#rtdbcoveragestatus) | - |
|
|
685
762
|
| <a id="unsupported-5"></a> `unsupported` | `number` | - |
|
|
686
763
|
|
|
@@ -926,7 +1003,7 @@ throws when `firebase.json` deploys none to the default instance.
|
|
|
926
1003
|
| :------ | :------ |
|
|
927
1004
|
| <a id="code"></a> `code` | `string` |
|
|
928
1005
|
| <a id="message"></a> `message` | `string` |
|
|
929
|
-
| <a id="path-
|
|
1006
|
+
| <a id="path-4"></a> `path` | `string` |
|
|
930
1007
|
| <a id="rule-2"></a> `rule` | [`RtdbRulesFindingRule`](#rtdbrulesfindingrule-1) |
|
|
931
1008
|
|
|
932
1009
|
***
|
|
@@ -993,7 +1070,7 @@ returns an empty `trace` and surfaces the wire text on `TestResult.notes`.
|
|
|
993
1070
|
| <a id="line-4"></a> `line?` | `number` | 1-indexed source line, when known. |
|
|
994
1071
|
| <a id="message-2"></a> `message` | `string` | Human-readable description. |
|
|
995
1072
|
| <a id="origin"></a> `origin` | [`RuleIssueOrigin`](#ruleissueorigin-1) | - |
|
|
996
|
-
| <a id="path-
|
|
1073
|
+
| <a id="path-5"></a> `path?` | `string` | Rules path the issue applies to, when known (e.g. `'/users/{uid}'`). |
|
|
997
1074
|
| <a id="rule-3"></a> `rule?` | `".read"` \| `".write"` \| `".validate"` | The Realtime Database rule at `path` the issue applies to. Absent for Firestore issues and for an RTDB issue about the whole ruleset. |
|
|
998
1075
|
| <a id="severity"></a> `severity` | [`RuleIssueSeverity`](#ruleissueseverity-1) | - |
|
|
999
1076
|
|
|
@@ -1009,7 +1086,7 @@ Context passed to the callback overload of ruleset().
|
|
|
1009
1086
|
|
|
1010
1087
|
| Property | Type |
|
|
1011
1088
|
| :------ | :------ |
|
|
1012
|
-
| <a id="path-
|
|
1089
|
+
| <a id="path-6"></a> `path` | (`path`: `string`, `def`: [`PathDef`](#pathdef)) => `void` |
|
|
1013
1090
|
|
|
1014
1091
|
***
|
|
1015
1092
|
|
|
@@ -1035,7 +1112,7 @@ expression the request was evaluating when the budget ran out.
|
|
|
1035
1112
|
| Property | Type |
|
|
1036
1113
|
| :------ | :------ |
|
|
1037
1114
|
| <a id="column-2"></a> `column?` | `number` |
|
|
1038
|
-
| <a id="kind-
|
|
1115
|
+
| <a id="kind-5"></a> `kind` | `"expressions"` \| `"document-lookups"` |
|
|
1039
1116
|
| <a id="limit"></a> `limit` | `number` |
|
|
1040
1117
|
| <a id="line-5"></a> `line?` | `number` |
|
|
1041
1118
|
| <a id="message-3"></a> `message` | `string` |
|
|
@@ -1208,6 +1285,16 @@ type RtdbRuleEvaluation = z.infer<typeof RtdbRuleEvaluationSchema>;
|
|
|
1208
1285
|
|
|
1209
1286
|
***
|
|
1210
1287
|
|
|
1288
|
+
<a id="rtdbrulekind"></a>
|
|
1289
|
+
|
|
1290
|
+
### RtdbRuleKind
|
|
1291
|
+
|
|
1292
|
+
```ts
|
|
1293
|
+
type RtdbRuleKind = ".read" | ".write" | ".validate" | ".indexOn";
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
***
|
|
1297
|
+
|
|
1211
1298
|
<a id="rtdbrulesfindingrule-1"></a>
|
|
1212
1299
|
|
|
1213
1300
|
### RtdbRulesFindingRule
|
|
@@ -2363,6 +2450,31 @@ A `bytes` value.
|
|
|
2363
2450
|
|
|
2364
2451
|
***
|
|
2365
2452
|
|
|
2453
|
+
<a id="canonicalrtdbexpression"></a>
|
|
2454
|
+
|
|
2455
|
+
### canonicalRtdbExpression()
|
|
2456
|
+
|
|
2457
|
+
```ts
|
|
2458
|
+
function canonicalRtdbExpression(raw: string): string;
|
|
2459
|
+
```
|
|
2460
|
+
|
|
2461
|
+
Canonical text of an RTDB rule expression: two expressions with equal
|
|
2462
|
+
canonical texts evaluate identically for every request. Deterministic and
|
|
2463
|
+
idempotent. Throws [RtdbExpressionSyntaxError](#rtdbexpressionsyntaxerror) for text that is not a
|
|
2464
|
+
rule expression.
|
|
2465
|
+
|
|
2466
|
+
#### Parameters
|
|
2467
|
+
|
|
2468
|
+
| Parameter | Type |
|
|
2469
|
+
| :------ | :------ |
|
|
2470
|
+
| `raw` | `string` |
|
|
2471
|
+
|
|
2472
|
+
#### Returns
|
|
2473
|
+
|
|
2474
|
+
`string`
|
|
2475
|
+
|
|
2476
|
+
***
|
|
2477
|
+
|
|
2366
2478
|
<a id="definertdbrules"></a>
|
|
2367
2479
|
|
|
2368
2480
|
### defineRtdbRules()
|
|
@@ -2383,6 +2495,33 @@ function defineRtdbRules(definition: RtdbRulesDefinition): RtdbRulesDocument;
|
|
|
2383
2495
|
|
|
2384
2496
|
***
|
|
2385
2497
|
|
|
2498
|
+
<a id="diffrtdbrules"></a>
|
|
2499
|
+
|
|
2500
|
+
### diffRtdbRules()
|
|
2501
|
+
|
|
2502
|
+
```ts
|
|
2503
|
+
function diffRtdbRules(
|
|
2504
|
+
deployed: Record<string, unknown>,
|
|
2505
|
+
local: Record<string, unknown>,
|
|
2506
|
+
options?: DiffRtdbRulesOptions): RtdbRuleChange[];
|
|
2507
|
+
```
|
|
2508
|
+
|
|
2509
|
+
Node-level diff of two `rules` trees, ordered by path then rule kind.
|
|
2510
|
+
|
|
2511
|
+
#### Parameters
|
|
2512
|
+
|
|
2513
|
+
| Parameter | Type |
|
|
2514
|
+
| :------ | :------ |
|
|
2515
|
+
| `deployed` | `Record`\<`string`, `unknown`\> |
|
|
2516
|
+
| `local` | `Record`\<`string`, `unknown`\> |
|
|
2517
|
+
| `options?` | [`DiffRtdbRulesOptions`](#diffrtdbrulesoptions) |
|
|
2518
|
+
|
|
2519
|
+
#### Returns
|
|
2520
|
+
|
|
2521
|
+
[`RtdbRuleChange`](#rtdbrulechange)[]
|
|
2522
|
+
|
|
2523
|
+
***
|
|
2524
|
+
|
|
2386
2525
|
<a id="duration"></a>
|
|
2387
2526
|
|
|
2388
2527
|
### duration()
|
|
@@ -68,6 +68,61 @@ html{scrollbar-gutter:stable}
|
|
|
68
68
|
<li><code>pyric database rules validate database.rules.json</code> prints the error findings only. It exits <code>2</code> when there is one or when the file is not rules JSON, <code>1</code> when the file can’t be read, and <code>0</code> otherwise.</li>
|
|
69
69
|
<li><code>pyric rules simulate --service database --rules-file database.rules.json --operation read --path /notes/n1</code> evaluates one request against the file.</li>
|
|
70
70
|
</ul>
|
|
71
|
+
<h2 id="compare-compiled-rules-with-the-rules-you-wrote-by-hand">Compare compiled rules with the rules you wrote by hand</h2>
|
|
72
|
+
<p>When you move hand-written rules to TypeScript, the compiled JSON spells the same rules differently: <code>auth != null</code> where you wrote <code>auth.uid !== null</code>, <code>child('a').child($id)</code> where you wrote <code>child('a/' + $id)</code>. <code>diffRtdbRules</code> with <code>ignoreEquivalent</code> reports only the rules that can decide differently:</p>
|
|
73
|
+
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#C678DD">import</span><span style="color:#ABB2BF"> { </span><span style="color:#E06C75">readFileSync</span><span style="color:#ABB2BF"> } </span><span style="color:#C678DD">from</span><span style="color:#98C379"> 'node:fs'</span><span style="color:#ABB2BF">;</span></span>
|
|
74
|
+
<span class="line"><span style="color:#C678DD">import</span><span style="color:#ABB2BF"> { </span><span style="color:#E06C75">diffRtdbRules</span><span style="color:#ABB2BF">, </span><span style="color:#E06C75">canonicalRtdbExpression</span><span style="color:#ABB2BF"> } </span><span style="color:#C678DD">from</span><span style="color:#98C379"> 'pyric/rules'</span><span style="color:#ABB2BF">;</span></span>
|
|
75
|
+
<span class="line"></span>
|
|
76
|
+
<span class="line"><span style="color:#C678DD">const</span><span style="color:#E5C07B"> handWritten</span><span style="color:#56B6C2"> =</span><span style="color:#E5C07B"> JSON</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">parse</span><span style="color:#ABB2BF">(</span><span style="color:#61AFEF">readFileSync</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">'legacy/database.rules.json'</span><span style="color:#ABB2BF">, </span><span style="color:#98C379">'utf8'</span><span style="color:#ABB2BF">)).</span><span style="color:#E06C75">rules</span><span style="color:#ABB2BF">;</span></span>
|
|
77
|
+
<span class="line"><span style="color:#C678DD">const</span><span style="color:#E5C07B"> compiled</span><span style="color:#56B6C2"> =</span><span style="color:#E5C07B"> rules</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">toJSON</span><span style="color:#ABB2BF">().</span><span style="color:#E06C75">rules</span><span style="color:#ABB2BF">;</span></span>
|
|
78
|
+
<span class="line"></span>
|
|
79
|
+
<span class="line"><span style="color:#61AFEF">diffRtdbRules</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75">handWritten</span><span style="color:#ABB2BF">, </span><span style="color:#E06C75">compiled</span><span style="color:#ABB2BF">); </span><span style="color:#7F848E;font-style:italic">// every rule whose text differs</span></span>
|
|
80
|
+
<span class="line"><span style="color:#61AFEF">diffRtdbRules</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75">handWritten</span><span style="color:#ABB2BF">, </span><span style="color:#E06C75">compiled</span><span style="color:#ABB2BF">, { </span><span style="color:#E06C75">ignoreEquivalent</span><span style="color:#ABB2BF">: </span><span style="color:#D19A66">true</span><span style="color:#ABB2BF"> }); </span><span style="color:#7F848E;font-style:italic">// only rules that can decide differently</span></span>
|
|
81
|
+
<span class="line"><span style="color:#7F848E;font-style:italic">// → [{ path, kind: '.read' | '.write' | '.validate' | '.indexOn', change: 'added' | 'removed' | 'changed', deployed?, local? }]</span></span>
|
|
82
|
+
<span class="line"></span>
|
|
83
|
+
<span class="line"><span style="color:#61AFEF">canonicalRtdbExpression</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"$uid === auth.uid"</span><span style="color:#ABB2BF">); </span><span style="color:#7F848E;font-style:italic">// "$uid == auth.uid"</span></span>
|
|
84
|
+
<span class="line"><span style="color:#61AFEF">canonicalRtdbExpression</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">"auth.uid !== null"</span><span style="color:#ABB2BF">); </span><span style="color:#7F848E;font-style:italic">// "!(auth == null)"</span></span></code></pre>
|
|
85
|
+
<p>A changed entry keeps both expressions exactly as written. Without the option the diff compares text.</p>
|
|
86
|
+
<p>Two expressions are equivalent when their canonical texts are equal. The canonical form applies only rewrites that cannot change a decision, errors included (a rule that errors fails, and <code>||</code> or <code>!</code> does not rescue it):</p>
|
|
87
|
+
<div class="table-scroll">
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
<table><thead><tr><th>Treated as the same</th><th>Why</th></tr></thead><tbody><tr><td>whitespace, grouping parentheses, <code>a && (b && c)</code> and <code>a && b && c</code></td><td>parentheses only shape the tree; <code>&&</code> and <code>||</code> evaluate left to right either way</td></tr><tr><td><code>"a"</code> and <code>'a'</code>, for a string literal written without a backslash</td><td>the same string; a literal with any escape (<code>'\d'</code>, <code>'\u0041'</code>) keeps its spelling, since production’s reading of most escapes has not been observed</td></tr><tr><td><code>===</code> and <code>==</code>, <code>!==</code> and <code>!=</code></td><td>RTDB compares without type conversion in both</td></tr><tr><td><code>auth.uid == null</code> and <code>auth == null</code></td><td><code>auth</code> is null exactly when signed out, and a signed-in <code>auth</code> always has a uid</td></tr><tr><td><code>child('a/b/' + $id + '/c')</code> and <code>child('a').child('b').child($id).child('c')</code></td><td>a <code>$</code> variable is exactly one key, and <code>child()</code> reads <code>/</code> as a separator</td></tr><tr><td><code>a != b</code> and <code>!(a == b)</code></td><td>both fail on the same inputs; otherwise <code>==</code> gives a boolean</td></tr><tr><td><code>a == b</code> and <code>b == a</code></td><td>equality is symmetric; operands that could fail other than with a rule error, or that index into an array, keep their order</td></tr></tbody></table></div>
|
|
125
|
+
<p>Deliberately not treated as the same: a concatenated part that is not a string literal or a <code>$</code> variable, <code>auth.uid</code> included (<code>child('m/' + auth.uid)</code> is not <code>child('m').child(auth.uid)</code>, because production has not been observed reading a uid such as <code>/x</code> or <code>a//b</code> with empty segments the same way in both forms); a literal glued to a variable (<code>'user_' + $id</code>) or written with an escape; empty segments and key-invalid characters in a path; the order of <code>&&</code> and <code>||</code> operands; <code>>=</code> and <code>></code>; double negation; and arithmetic or constant folding. A rule that falls in one of those is reported as changed, so read those entries yourself.</p>
|
|
71
126
|
<h2 id="turn-enforcement-from-a-deployed-game">Turn enforcement, from a deployed game</h2>
|
|
72
127
|
<p>This turn-enforcement example is built from the same parts:</p>
|
|
73
128
|
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#C678DD">import</span><span style="color:#ABB2BF"> {</span></span>
|
|
@@ -93,4 +148,4 @@ html{scrollbar-gutter:stable}
|
|
|
93
148
|
<h2 id="and-from-an-agent">And from an agent</h2>
|
|
94
149
|
<p>An agent authors and checks the same way you do: lint and simulate locally (<code>pyric rules lint --service database</code>, <code>pyric rules simulate --service database</code>, or <code>rtdb_simulate_access</code> against the connected sandbox), generate JSON with <code>pyric database rules generate</code>, then ship with <code>firebase-tools</code>. See the RTDB task in <a href="../../agent/work-with-an-agent/">Work with an agent</a>.</p>
|
|
95
150
|
<h2 id="where-to-go-next">Where to go next</h2>
|
|
96
|
-
<p>The full builder catalog, with the exact expression each one produces, is in the <a href="../../pyric-database-reference-api/">constraints reference</a>. For matches, turns, results, counters, presence, rate limits and validated shapes, compose the <a href="../rtdb-rules-standard-library/">RTDB rules standard library</a> instead of writing the expressions yourself. For the data side of RTDB, see <a href="../../build/realtime-database/">Sync realtime data</a>.</p> </article> </main> <aside class="doc-toc" aria-label="On this page" data-astro-cid-mw7aashj> <h2 class="doc-toc-heading" data-astro-cid-mw7aashj>On this page</h2> <ul data-astro-cid-mw7aashj> <li data-depth="2" data-astro-cid-mw7aashj> <a href="#define-the-rules-in-code" data-astro-cid-mw7aashj>Define the rules in code</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#check-it-before-anything-runs" data-astro-cid-mw7aashj>Check it before anything runs</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#simulate-a-request" data-astro-cid-mw7aashj>Simulate a request</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#ship-the-json" data-astro-cid-mw7aashj>Ship the JSON</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#turn-enforcement-from-a-deployed-game" data-astro-cid-mw7aashj>Turn enforcement, from a deployed game</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#and-from-an-agent" data-astro-cid-mw7aashj>And from an agent</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#where-to-go-next" data-astro-cid-mw7aashj>Where to go next</a> </li> </ul> </aside> </div> <script type="module">const a=document.querySelector("[data-api-symbol-filter]"),d=[...document.querySelectorAll("[data-api-symbol-group]")],n=document.querySelector("[data-api-symbol-empty]");a?.addEventListener("input",()=>{const t=a.value.trim().toLowerCase();let o=0;for(const i of d){let e=0;i.querySelectorAll("[data-api-symbol]").forEach(l=>{const r=!t||(l.dataset.apiSymbol??"").includes(t);l.hidden=!r,r&&e++}),i.hidden=e===0,o+=e}n&&(n.hidden=o>0)});</script> <script type="module">const s=new Map;document.querySelectorAll('.doc-toc a[href^="#"]').forEach(t=>s.set(decodeURIComponent(t.hash.slice(1)),t));if(s.size>0){const t=new Set,n=()=>{let o=null;for(const e of s.keys())if(t.has(e)){o=e;break}s.forEach((e,c)=>e.classList.toggle("active",c===o))},r=new IntersectionObserver(o=>{for(const e of o)e.isIntersecting?t.add(e.target.id):t.delete(e.target.id);n()},{rootMargin:"-72px 0px -60% 0px"});s.forEach((o,e)=>{const c=document.getElementById(e);c&&r.observe(c)})}</script> <script type="module">document.querySelector('.studio__nav [aria-current="page"]')?.scrollIntoView({block:"nearest",inline:"center"});</script> </body></html>
|
|
151
|
+
<p>The full builder catalog, with the exact expression each one produces, is in the <a href="../../pyric-database-reference-api/">constraints reference</a>. For matches, turns, results, counters, presence, rate limits and validated shapes, compose the <a href="../rtdb-rules-standard-library/">RTDB rules standard library</a> instead of writing the expressions yourself. For the data side of RTDB, see <a href="../../build/realtime-database/">Sync realtime data</a>.</p> </article> </main> <aside class="doc-toc" aria-label="On this page" data-astro-cid-mw7aashj> <h2 class="doc-toc-heading" data-astro-cid-mw7aashj>On this page</h2> <ul data-astro-cid-mw7aashj> <li data-depth="2" data-astro-cid-mw7aashj> <a href="#define-the-rules-in-code" data-astro-cid-mw7aashj>Define the rules in code</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#check-it-before-anything-runs" data-astro-cid-mw7aashj>Check it before anything runs</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#simulate-a-request" data-astro-cid-mw7aashj>Simulate a request</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#ship-the-json" data-astro-cid-mw7aashj>Ship the JSON</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#compare-compiled-rules-with-the-rules-you-wrote-by-hand" data-astro-cid-mw7aashj>Compare compiled rules with the rules you wrote by hand</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#turn-enforcement-from-a-deployed-game" data-astro-cid-mw7aashj>Turn enforcement, from a deployed game</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#and-from-an-agent" data-astro-cid-mw7aashj>And from an agent</a> </li><li data-depth="2" data-astro-cid-mw7aashj> <a href="#where-to-go-next" data-astro-cid-mw7aashj>Where to go next</a> </li> </ul> </aside> </div> <script type="module">const a=document.querySelector("[data-api-symbol-filter]"),d=[...document.querySelectorAll("[data-api-symbol-group]")],n=document.querySelector("[data-api-symbol-empty]");a?.addEventListener("input",()=>{const t=a.value.trim().toLowerCase();let o=0;for(const i of d){let e=0;i.querySelectorAll("[data-api-symbol]").forEach(l=>{const r=!t||(l.dataset.apiSymbol??"").includes(t);l.hidden=!r,r&&e++}),i.hidden=e===0,o+=e}n&&(n.hidden=o>0)});</script> <script type="module">const s=new Map;document.querySelectorAll('.doc-toc a[href^="#"]').forEach(t=>s.set(decodeURIComponent(t.hash.slice(1)),t));if(s.size>0){const t=new Set,n=()=>{let o=null;for(const e of s.keys())if(t.has(e)){o=e;break}s.forEach((e,c)=>e.classList.toggle("active",c===o))},r=new IntersectionObserver(o=>{for(const e of o)e.isIntersecting?t.add(e.target.id):t.delete(e.target.id);n()},{rootMargin:"-72px 0px -60% 0px"});s.forEach((o,e)=>{const c=document.getElementById(e);c&&r.observe(c)})}</script> <script type="module">document.querySelector('.studio__nav [aria-current="page"]')?.scrollIntoView({block:"nearest",inline:"center"});</script> </body></html>
|
|
@@ -82,6 +82,39 @@ The CLI checks the JSON file itself, so CI can gate on it without TypeScript in
|
|
|
82
82
|
- `pyric database rules validate database.rules.json` prints the error findings only. It exits `2` when there is one or when the file is not rules JSON, `1` when the file can't be read, and `0` otherwise.
|
|
83
83
|
- `pyric rules simulate --service database --rules-file database.rules.json --operation read --path /notes/n1` evaluates one request against the file.
|
|
84
84
|
|
|
85
|
+
## Compare compiled rules with the rules you wrote by hand
|
|
86
|
+
|
|
87
|
+
When you move hand-written rules to TypeScript, the compiled JSON spells the same rules differently: `auth != null` where you wrote `auth.uid !== null`, `child('a').child($id)` where you wrote `child('a/' + $id)`. `diffRtdbRules` with `ignoreEquivalent` reports only the rules that can decide differently:
|
|
88
|
+
```ts
|
|
89
|
+
import { readFileSync } from 'node:fs';
|
|
90
|
+
import { diffRtdbRules, canonicalRtdbExpression } from 'pyric/rules';
|
|
91
|
+
|
|
92
|
+
const handWritten = JSON.parse(readFileSync('legacy/database.rules.json', 'utf8')).rules;
|
|
93
|
+
const compiled = rules.toJSON().rules;
|
|
94
|
+
|
|
95
|
+
diffRtdbRules(handWritten, compiled); // every rule whose text differs
|
|
96
|
+
diffRtdbRules(handWritten, compiled, { ignoreEquivalent: true }); // only rules that can decide differently
|
|
97
|
+
// → [{ path, kind: '.read' | '.write' | '.validate' | '.indexOn', change: 'added' | 'removed' | 'changed', deployed?, local? }]
|
|
98
|
+
|
|
99
|
+
canonicalRtdbExpression("$uid === auth.uid"); // "$uid == auth.uid"
|
|
100
|
+
canonicalRtdbExpression("auth.uid !== null"); // "!(auth == null)"
|
|
101
|
+
```
|
|
102
|
+
A changed entry keeps both expressions exactly as written. Without the option the diff compares text.
|
|
103
|
+
|
|
104
|
+
Two expressions are equivalent when their canonical texts are equal. The canonical form applies only rewrites that cannot change a decision, errors included (a rule that errors fails, and `||` or `!` does not rescue it):
|
|
105
|
+
|
|
106
|
+
| Treated as the same | Why |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| whitespace, grouping parentheses, `a && (b && c)` and `a && b && c` | parentheses only shape the tree; `&&` and `\|\|` evaluate left to right either way |
|
|
109
|
+
| `"a"` and `'a'`, for a string literal written without a backslash | the same string; a literal with any escape (`'\d'`, `'\u0041'`) keeps its spelling, since production's reading of most escapes has not been observed |
|
|
110
|
+
| `===` and `==`, `!==` and `!=` | RTDB compares without type conversion in both |
|
|
111
|
+
| `auth.uid == null` and `auth == null` | `auth` is null exactly when signed out, and a signed-in `auth` always has a uid |
|
|
112
|
+
| `child('a/b/' + $id + '/c')` and `child('a').child('b').child($id).child('c')` | a `$` variable is exactly one key, and `child()` reads `/` as a separator |
|
|
113
|
+
| `a != b` and `!(a == b)` | both fail on the same inputs; otherwise `==` gives a boolean |
|
|
114
|
+
| `a == b` and `b == a` | equality is symmetric; operands that could fail other than with a rule error, or that index into an array, keep their order |
|
|
115
|
+
|
|
116
|
+
Deliberately not treated as the same: a concatenated part that is not a string literal or a `$` variable, `auth.uid` included (`child('m/' + auth.uid)` is not `child('m').child(auth.uid)`, because production has not been observed reading a uid such as `/x` or `a//b` with empty segments the same way in both forms); a literal glued to a variable (`'user_' + $id`) or written with an escape; empty segments and key-invalid characters in a path; the order of `&&` and `||` operands; `>=` and `>`; double negation; and arithmetic or constant folding. A rule that falls in one of those is reported as changed, so read those entries yourself.
|
|
117
|
+
|
|
85
118
|
## Turn enforcement, from a deployed game
|
|
86
119
|
|
|
87
120
|
This turn-enforcement example is built from the same parts:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
<!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"><title>Play chess against Security Rules</title><link rel="icon" type="image/svg+xml" href="/__pyric/ui/pyric-logo.svg"><link rel="preconnect" href="https://use.typekit.net" crossorigin><link rel="stylesheet" href="https://use.typekit.net/jwb3ycu.css"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"><style>html{scrollbar-gutter:stable}
|
|
2
2
|
</style>
|
|
3
3
|
<link rel="stylesheet" href="/__pyric/ui/_astro/_example_.B9DG9HLJ.css">
|
|
4
|
-
<link rel="stylesheet" href="/__pyric/ui/_astro/app.8TtSeQQX.css"></head> <body> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="
|
|
4
|
+
<link rel="stylesheet" href="/__pyric/ui/_astro/app.8TtSeQQX.css"></head> <body> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="sFXys" component-url="/__pyric/ui/_astro/chess-showcase.CZe_3fJZ.js" component-export="ChessShowcase" renderer-url="/__pyric/ui/_astro/client.D2ORs4B-.js" props="{}" ssr client="only" opts="{"name":"ChessShowcase","value":"react"}"></astro-island> </body></html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyric/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.26",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://pyric.dev",
|
|
6
6
|
"repository": {
|
|
@@ -245,12 +245,12 @@
|
|
|
245
245
|
},
|
|
246
246
|
"dependencies": {
|
|
247
247
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
248
|
-
"create-pyric": "^0.1.0-alpha.
|
|
248
|
+
"create-pyric": "^0.1.0-alpha.26",
|
|
249
249
|
"esbuild": "^0.28.0",
|
|
250
250
|
"fake-indexeddb": "^6.0.0",
|
|
251
251
|
"jsonc-parser": "^3.3.1",
|
|
252
|
-
"pyric": "^0.1.0-alpha.
|
|
253
|
-
"pyric-admin": "^0.1.0-alpha.
|
|
252
|
+
"pyric": "^0.1.0-alpha.26",
|
|
253
|
+
"pyric-admin": "^0.1.0-alpha.26",
|
|
254
254
|
"ws": "^8.18.0",
|
|
255
255
|
"zod": "^3.23.0"
|
|
256
256
|
},
|
|
@@ -9,25 +9,14 @@
|
|
|
9
9
|
* URL, and it is not returned in any result or message.
|
|
10
10
|
*/
|
|
11
11
|
import type { ToolHandler } from 'pyric/sandbox/internal';
|
|
12
|
+
import { diffRtdbRules, type RtdbRuleChange } from 'pyric/rules/internal/rtdb';
|
|
12
13
|
import { parseRtdbRulesText } from './rules-json.js';
|
|
13
14
|
import { HOSTED_CREDENTIAL_SOURCES } from '../credentials/node/scope.js';
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* One rule expression that differs. `deployed` is absent for `added` and
|
|
21
|
-
* `local` is absent for `removed`: the change is what deploying the local
|
|
22
|
-
* ruleset would do to the deployed one.
|
|
23
|
-
*/
|
|
24
|
-
export interface RtdbRuleChange {
|
|
25
|
-
path: string;
|
|
26
|
-
kind: RtdbRuleKind;
|
|
27
|
-
change: 'added' | 'removed' | 'changed';
|
|
28
|
-
deployed?: string;
|
|
29
|
-
local?: string;
|
|
30
|
-
}
|
|
16
|
+
// The diff itself is pure and lives in `pyric` (public from `pyric/rules`);
|
|
17
|
+
// it is re-exported here so this module's exports stay as they were.
|
|
18
|
+
export { DIFFED_RULE_KINDS, diffRtdbRules } from 'pyric/rules/internal/rtdb';
|
|
19
|
+
export type { DiffRtdbRulesOptions, RtdbRuleChange, RtdbRuleKind } from 'pyric/rules/internal/rtdb';
|
|
31
20
|
|
|
32
21
|
export interface InspectDeployedRtdbRulesOptions {
|
|
33
22
|
scope: { resolveToken(): Promise<string> };
|
|
@@ -96,67 +85,6 @@ export async function inspectDeployedRtdbRules(
|
|
|
96
85
|
return { ok: true, deployed, diff: diffRtdbRules(deployed.rules, local.rules) };
|
|
97
86
|
}
|
|
98
87
|
|
|
99
|
-
/** Node-level diff of two `rules` trees, ordered by path then rule kind. */
|
|
100
|
-
export function diffRtdbRules(
|
|
101
|
-
deployed: Record<string, unknown>,
|
|
102
|
-
local: Record<string, unknown>,
|
|
103
|
-
): RtdbRuleChange[] {
|
|
104
|
-
const deployedRules = collectRules(deployed);
|
|
105
|
-
const localRules = collectRules(local);
|
|
106
|
-
const changes: RtdbRuleChange[] = [];
|
|
107
|
-
for (const key of new Set([...deployedRules.keys(), ...localRules.keys()])) {
|
|
108
|
-
const d = deployedRules.get(key);
|
|
109
|
-
const l = localRules.get(key);
|
|
110
|
-
if (d?.value === l?.value) continue;
|
|
111
|
-
const node = (d ?? l)!;
|
|
112
|
-
const change: RtdbRuleChange = {
|
|
113
|
-
path: node.path,
|
|
114
|
-
kind: node.kind,
|
|
115
|
-
change: d === undefined ? 'added' : l === undefined ? 'removed' : 'changed',
|
|
116
|
-
};
|
|
117
|
-
if (d !== undefined) change.deployed = d.value;
|
|
118
|
-
if (l !== undefined) change.local = l.value;
|
|
119
|
-
changes.push(change);
|
|
120
|
-
}
|
|
121
|
-
return changes.sort((a, b) => compare(a.path, b.path) || compare(a.kind, b.kind));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const compare = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
125
|
-
|
|
126
|
-
interface CollectedRule {
|
|
127
|
-
path: string;
|
|
128
|
-
kind: RtdbRuleKind;
|
|
129
|
-
value: string;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function formatRuleValue(kind: RtdbRuleKind, value: unknown): string {
|
|
133
|
-
if (kind === '.indexOn') {
|
|
134
|
-
const normalized = typeof value === 'string' ? [value] : value;
|
|
135
|
-
return JSON.stringify(normalized);
|
|
136
|
-
}
|
|
137
|
-
// A boolean and its string form are one expression: `true` and "true".
|
|
138
|
-
return typeof value === 'string' ? value : JSON.stringify(value);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function collectRules(root: Record<string, unknown>): Map<string, CollectedRule> {
|
|
142
|
-
const out = new Map<string, CollectedRule>();
|
|
143
|
-
const walk = (node: Record<string, unknown>, path: string): void => {
|
|
144
|
-
for (const [key, child] of Object.entries(node)) {
|
|
145
|
-
if (key.startsWith('.')) {
|
|
146
|
-
if ((DIFFED_RULE_KINDS as readonly string[]).includes(key)) {
|
|
147
|
-
const kind = key as RtdbRuleKind;
|
|
148
|
-
const value = formatRuleValue(kind, child);
|
|
149
|
-
out.set(`${path}\u0000${kind}`, { path, kind, value });
|
|
150
|
-
}
|
|
151
|
-
} else if (typeof child === 'object' && child !== null && !Array.isArray(child)) {
|
|
152
|
-
walk(child as Record<string, unknown>, `${path === '/' ? '' : path}/${key}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
walk(root, '/');
|
|
157
|
-
return out;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
88
|
export interface RtdbInspectRulesToolDeps {
|
|
161
89
|
scope: { resolveToken(): Promise<string> };
|
|
162
90
|
databaseURL: string;
|