Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion .semgrep/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ text-scanning guardrail tests — chained call expressions split across lines.
|---|---|---|
| Shape | one legitimate owner file | many legitimate owners |
| Owner named via | `paths.exclude` | n/a |
| Current findings | **0** | hundreds (see `baseline-counts.json`) |
| Current findings | **0** | 92 (see `baseline-counts.json`) |
| CI gate | `--error`, whole tree | `--baseline-commit` (new findings only) + a per-rule count that may only go down |

**Adding a rule:** if the capability has one owner and you can get the count to
Expand All @@ -37,6 +37,28 @@ third, `fieldstay-supabase-unbounded-select-global-table` (tier 2c of the
severity ladder below), was promoted on 2026-08-07 — see that section for its
history.

**The Supabase error-handling family was promoted on 2026-08-11**:
`fieldstay-supabase-discarded-result`, `fieldstay-supabase-read-without-error`
and `fieldstay-supabase-read-without-error-fan-in`, all three at `ERROR` with
no `paths.exclude`. Nothing was fixed in that change — the burn-downs that took
them from 159 + 14 live sites to 0 are the 2026-08-07 and 2026-08-08 entries in
`baseline-counts.json`. It collected the gate upgrade those burn-downs had
earned and never taken, which matters because a rule left at 0 in `ratchet.yml`
gates only on `--baseline-commit`: a finding invisible in the diff view — a
file moved, a branch cut before the burn-down, a rewrite semgrep attributes to
neither side — still passes. `--error` across the whole tree has no such hole.

These three are the clearest case of *handling, not ownership*: no file
legitimately owns "discard a PostgREST error", so there is no owner to name in
a `paths.exclude`, and every exemption is already expressed as the handling
constructs themselves (binding the result, destructuring `error`, going through
`lib/supabase/unwrap.ts`). Fire-checked before promoting, same protocol as tiers
2 and 2c: a scratch fixture under `lib/` carried one deliberate violation per
rule **plus a correct control for each**, and semgrep reported exactly the three
violations and none of the three controls — the controls are the half that
matters, since a rule that fires on everything also "fires". Then reverted, and
the whole tree re-run at `--error` to confirm exit 0.

## Severity inside the ratchet family

A ratchet rule whose majority is permitted-by-policy is one people learn to
Expand Down
37 changes: 18 additions & 19 deletions .semgrep/baseline-counts.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"fieldstay-supabase-unbounded-select rule (284) into the severity ladder below,",
"and after fieldstay-role-filtered-membership-read (3 -> 0) and",
"fieldstay-untimed-external-fetch (1 -> 0) were fixed and PROMOTED to",
"chokepoints.yml which is why they no longer appear here.",
"chokepoints.yml \u2014 which is why they no longer appear here.",
"scripts/check-semgrep-ratchet.mjs re-runs the ratchet rules and FAILS if any",
"count is higher than the number here. Numbers may only go DOWN: fix sites,",
"then run `node scripts/check-semgrep-ratchet.mjs --update` to lock the",
Expand All @@ -30,14 +30,14 @@
"PROMOTED 2026-08-02: -cross-tenant reached 0 (see above) and moved to",
"chokepoints.yml, where it gates at --error across the whole tree instead of",
"only on findings new vs. the PR base. Its key is deleted here in the same",
"change, per the promotion rule in .semgrep/README.md a rule cannot be in",
"change, per the promotion rule in .semgrep/README.md \u2014 a rule cannot be in",
"both files, and check-semgrep-ratchet.mjs fails on a baseline entry whose",
"rule ratchet.yml no longer declares.",
"",
"BURNED DOWN 2026-08-07: -global-table 5 -> 0. Bounded all 5 sites with an",
"explicit .limit() (or fetchAllRows for platform-inventory-template-",
"broadcast.ts, matching its sibling inventory_catalog read in the same",
"function) properties/[id]/page.tsx (maintenance_catalog_items),",
"function) \u2014 properties/[id]/page.tsx (maintenance_catalog_items),",
"inventory/page.tsx (inventory_catalog), settings/integrations/page.tsx and",
"setup/pms/page.tsx (integration_providers). -read-without-error dropped",
"222 -> 221 as a side effect: fetchAllRows throws on a page error instead of",
Expand All @@ -54,7 +54,7 @@
"",
"BURNED DOWN 2026-08-07 (second pass, same day): -read-without-error-fan-in",
"14 -> 0 (all 14 Promise.all fan-ins now destructure and handle every",
"element's error, via throwIfAnyQueryFailed isRealQueryError-filtered",
"element's error, via throwIfAnyQueryFailed \u2014 isRealQueryError-filtered",
"where a .single() read's PGRST116 was already tolerated gracefully).",
"-read-without-error 221 -> 171 and -discarded-result 119 -> 87 (82 more",
"sites across 64 files, fanned out across four parallel batches covering",
Expand All @@ -64,11 +64,11 @@
"error.tsx, a Server Action's try/catch converts the throw to its own",
"{ error } return, a route handler throws bare or matches its own",
"try/catch, an Inngest step.run throws to retry just that step). Added",
"lib/supabase/unwrap.ts's isRealQueryError() export in the same pass the",
"lib/supabase/unwrap.ts's isRealQueryError() export in the same pass \u2014 the",
"PGRST116-vs-real-error filter needed by nearly every Inngest .single()",
"site was previously hand-rolled per call site (25 inline occurrences",
"already in the tree); this is that pattern's first shared helper.",
"-read-without-error-fan-in reached 0 but was NOT promoted in this pass ",
"-read-without-error-fan-in reached 0 but was NOT promoted in this pass \u2014",
"that needs its own fire-check/revert proof per .semgrep/README.md's",
"promotion protocol, same as -global-table above, done separately.",
"",
Expand All @@ -80,7 +80,7 @@
"",
"BURNED DOWN 2026-08-07 (fourth pass, same day): CI's --baseline-commit",
"semgrep job failed on 9 findings the fan-in fix's read-without-error",
"rewrite incidentally surfaced as NEW relative to the PR base the",
"rewrite incidentally surfaced as NEW relative to the PR base \u2014 the",
"underlying unbounded selects (-in-list, -org-scoped, -single-parent tiers)",
"already existed pre-rewrite and were already counted in these baselines,",
"but touching those exact lines to add error handling made semgrep's diff",
Expand All @@ -91,41 +91,41 @@
"is a correctness bug, not a display nicety) and on auto-assign-turnover's",
"property-turnovers read (same unbounded-over-lifetime shape its sibling",
"turnover_assignments read next to it already documented and paginated).",
"-in-list 36 -> 34, -org-scoped 101 -> 98, -single-parent 34 -> 30 9 net",
"-in-list 36 -> 34, -org-scoped 101 -> 98, -single-parent 34 -> 30 \u2014 9 net",
"across the two files with a double fix (seed-from-amenities.ts,",
"auto-assign-turnover.ts) plus the 7 single-finding files. Verified",
"against the exact CI invocation: `semgrep --config .semgrep/ratchet.yml",
"--baseline-commit <merge-base> --json` returns zero results.",
"",
"BURNED DOWN 2026-08-08: -read-without-error 94 -> 0 and -discarded-result",
"65 -> 0 both fully cleared, platform-wide. 159 sites across 43 files,",
"65 -> 0 \u2014 both fully cleared, platform-wide. 159 sites across 43 files,",
"fanned out across four parallel batches (~40 sites each) covering",
"non-overlapping file sets, same toolkit as the prior 2026-08-07 pass",
"(unwrap/unwrapList/tryUnwrap/tryUnwrapList/throwIfAnyQueryFailed/",
"reportQueryError/isRealQueryError from lib/supabase/unwrap.ts). Fixed 4",
"sonarjs warnings the added error-handling branches pushed over threshold",
"(cognitive-complexity/nested-control-flow) by extracting named helpers ",
"(cognitive-complexity/nested-control-flow) by extracting named helpers \u2014",
"app/(dashboard)/settings/team/actions.ts's findAlreadyMemberError,",
"app/api/support/chat/route.ts's resolveConversationId,",
"lib/checklists/apply-master-template.ts's getOrCreateTemplateId/",
"insertComposedSections, lib/inngest/functions/ownerrez/",
"ownerrez-reviews-sync.ts's notifyRevokedThrottled net -5 vs the prior",
"ownerrez-reviews-sync.ts's notifyRevokedThrottled \u2014 net -5 vs the prior",
"ceiling since the last extraction also cleared a second pre-existing",
"warning for free; package.json's max-warnings lowered 191 -> 190 to lock",
"it in. Both rules are now eligible for promotion to chokepoints.yml (same",
"as -global-table and potentially -read-without-error-fan-in before it) ",
"as -global-table and potentially -read-without-error-fan-in before it) \u2014",
"not done in this pass; needs its own fire-check/revert proof per",
".semgrep/README.md's promotion protocol.",
"",
"BURNED DOWN 2026-08-08 (second pass, same day): CI's --baseline-commit",
"semgrep job failed a second time, same root cause as the 2026-08-07 CI",
"fix the read-without-error/discarded-result rewrite touched lines that",
"fix \u2014 the read-without-error/discarded-result rewrite touched lines that",
"already had pre-existing unbounded selects (-in-list/-org-scoped/",
"-single-parent tiers), so semgrep's diff view treated 26 of them as new",
"relative to the PR base even though they were already counted here.",
"Bounded all 26 for real across 22 files with an explicit .limit()",
"(mostly bounded by a caller-supplied id list, a fixed per-parent set, or a",
"documented per-org page-render cap of 500) properties/vendors/sponsors",
"documented per-org page-render cap of 500) \u2014 properties/vendors/sponsors",
"list reads across page.tsx Server Components, PMS-sync property lookups",
"(ownerrez/hostaway/hospitable), checklist-section and push-subscription",
"reads. -in-list 23 -> 13, -org-scoped 85 -> 76, -single-parent 25 -> 18.",
Expand All @@ -135,13 +135,12 @@
"unbounded-select.test.ts's separate shrink-only baseline",
"(checklist-broadcast.ts, hospitable-reviews-backfill.ts,",
"hostaway/initial-sync.ts, ownerrez-reviews-sync.ts) as a side effect of",
"the same .limit() additions."
"the same .limit() additions.",
"",
"PROMOTED 2026-08-11: the three Supabase error-handling rules (-discarded-result, -read-without-error, -read-without-error-fan-in) all sat at 0 and moved to chokepoints.yml, where they gate at --error across the whole tree rather than only on findings new vs. the PR base. Their keys are deleted here in the same change, per the promotion rule in .semgrep/README.md. No site was fixed in this change -- the burn-downs that got them to 0 are the 2026-08-07 and 2026-08-08 entries above; this is only the gate upgrade those burn-downs earned and never collected. Fire-checked before promoting, same protocol as -cross-tenant and -global-table: a scratch fixture with one deliberate violation per rule plus a correct control for each produced exactly 3 findings (the violations) and 0 on the controls, then was reverted. What remains in this file is the unbounded-select ladder alone: -in-list 11, -org-scoped 65, -single-parent 16."
],
"measured_at": "2026-08-10",
"measured_at": "2026-08-11",
"counts": {
"fieldstay-supabase-discarded-result": 0,
"fieldstay-supabase-read-without-error": 0,
"fieldstay-supabase-read-without-error-fan-in": 0,
"fieldstay-supabase-unbounded-select-in-list": 11,
"fieldstay-supabase-unbounded-select-org-scoped": 65,
"fieldstay-supabase-unbounded-select-single-parent": 16
Expand Down
98 changes: 98 additions & 0 deletions .semgrep/chokepoints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -332,3 +332,101 @@ rules:
- metavariable-regex:
metavariable: $T
regex: ^["'](?:asset_type_standards|integration_providers|inventory_catalog|maintenance_catalog_items|oauth_states|pending_integration_links|pending_oauth_authorizations|platform_admins|platform_inventory_template_items|platform_inventory_templates|platform_seed_room_template_items|platform_seed_room_templates|platform_staff|processed_webhooks|profiles|promo_hospitable_launch_counter|stripe_processed_events|support_kb_chunks)["']$

# ══ PROMOTED FROM ratchet.yml 2026-08-11 ═════════════════════════════════
# The three rules below are the Supabase error-handling family. They were
# ratchet rules with 159 + 14 live sites as recently as 2026-08-08; the
# burn-downs recorded in baseline-counts.json cleared them to 0 across the
# whole tree, so they now gate at --error everywhere instead of only on
# findings new vs. the PR base.
#
# Why these are chokepoints with NO paths.exclude: unlike the service-role
# key or Telnyx, no file "owns" the capability of discarding a PostgREST
# error — there is no legitimate owner to name. Every exemption is expressed
# purely as the handling constructs in the patterns themselves (binding the
# result, destructuring `error`, going through lib/supabase/unwrap.ts).
# That is the paths.exclude-vs-pattern-not-inside distinction in the README:
# this is handling, not ownership.
#
# Before promoting, each was confirmed to still FIRE — a rule at zero because
# it is broken is indistinguishable from one at zero because the tree is
# clean. A scratch fixture under lib/ carried one deliberate violation per
# rule plus a correct control for each; semgrep reported exactly the three
# violations and none of the three controls, then the fixture was reverted.
# Same protocol as the -cross-tenant and -global-table promotions.
#
# Their baseline-counts.json keys were deleted in the same change, per the
# promotion rule in .semgrep/README.md.
# ── A discarded PostgREST result discards the ERROR ──────────────────────
# `await supabase.from(x).update(y)` resolves with { error } instead of
# throwing, so a surrounding try/catch cannot see the failure and neither
# can Sentry. Highest-precision rule in this file.
- id: fieldstay-supabase-discarded-result
languages: [typescript]
severity: ERROR
message: >-
This write's result is discarded. PostgREST RESOLVES with { error } — it
does not throw — so a surrounding try/catch cannot catch it and nothing
reaches Sentry. Destructure { error } and branch, or use the
lib/supabase/unwrap.ts helpers.
patterns:
- pattern: await <... $S.from($T).$OP(...) ...>;
- metavariable-regex:
metavariable: $OP
regex: ^(insert|update|upsert|delete|select|rpc)$
- pattern-not-inside: $X = await ...
- pattern-not-inside: return await ...
- pattern-not-inside: const $X = await ...
- pattern-not-inside: let $X = await ...
- pattern-not-inside: var $X = await ...
- pattern-not-inside: $F(await ...)
- pattern-not-inside: await Promise.$M(...)

# ── A read destructured without its error ────────────────────────────────
# `const { data } = await …` collapses "the query errored" and "zero rows"
# into the same null, so an RLS/GRANT regression renders a friendly empty
# state with nothing logged. This is the ~481-site class that
# unit/guardrails/supabase-error-handling.test.ts baselines.
- id: fieldstay-supabase-read-without-error
languages: [typescript]
severity: ERROR
message: >-
`data` destructured without `error`. A failed query and an empty table
are now indistinguishable. Use unwrap/unwrapList/tryUnwrap from
lib/supabase/unwrap.ts, or destructure { data, error } and branch.
patterns:
- pattern-either:
- pattern: "const { data } = await $Q"
- pattern: "const { data: $D } = await $Q"
- pattern: "const { data, count: $C } = await $Q"
- pattern: "const { data: $D, count: $C } = await $Q"
# Semgrep's JS object patterns match PARTIALLY — `const { data } = …`
# also matches `const { data, error } = …`. Without these two negations
# the rule reports every read in the repo, half of them already correct.
- pattern-not: "const {..., error, ...} = await $Q"
- pattern-not: "const {..., error: $E, ...} = await $Q"
- metavariable-pattern:
metavariable: $Q
pattern: <... $S.from($T) ...>
Comment on lines +398 to +410

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

cat > "$tmpdir/supabase-error-handling-fixture.ts" <<'EOF'
async function verifyCoverage() {
  let { data } = await supabase.from('items').select()
  const { count } = await supabase.from('items').select('*', { count: 'exact' })

  let [{ data: batchData }] = await Promise.all([
    supabase.from('items').select(),
  ])
  const [{ count: batchCount }] = await Promise.all([
    supabase.from('items').select('*', { count: 'exact' }),
  ])

  let { data: handledData, error: handledError } = await supabase.from('items').select()
  const [{ count: handledCount, error: handledBatchError }] = await Promise.all([
    supabase.from('items').select('*', { count: 'exact' }),
  ])
}
EOF

semgrep --config .semgrep/chokepoints.yml --json "$tmpdir" |
  jq '[.results[] | select(
    .check_id == "fieldstay-supabase-read-without-error" or
    .check_id == "fieldstay-supabase-read-without-error-fan-in"
  )] | length'

# Expect 4 findings after the rule expansion and zero findings for the handled controls.

Repository: smj1860/fieldstay

Length of output: 605


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- relevant rules ---'
sed -n '360,455p' .semgrep/chokepoints.yml

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

cat > "$tmpdir/supabase-error-handling-fixture.ts" <<'EOF'
async function verifyCoverage() {
  let { data } = await supabase.from('items').select()
  var { data: aliasedData } = await supabase.from('items').select()
  const { count } = await supabase.from('items').select('*', { count: 'exact' })
  let { count: aliasedCount } = await supabase.from('items').select('*', { count: 'exact' })

  let [{ data: batchData }] = await Promise.all([
    supabase.from('items').select(),
  ])
  var [{ data: varBatchData }] = await Promise.all([
    supabase.from('items').select(),
  ])
  const [{ count: batchCount }] = await Promise.all([
    supabase.from('items').select('*', { count: 'exact' }),
  ])
  let [{ count: letBatchCount }] = await Promise.all([
    supabase.from('items').select('*', { count: 'exact' }),
  ])

  let { data: handledData, error: handledError } = await supabase.from('items').select()
  const [{ count: handledCount, error: handledBatchError }] = await Promise.all([
    supabase.from('items').select('*', { count: 'exact' }),
  ])
}
EOF

semgrep --config .semgrep/chokepoints.yml --json "$tmpdir" > "$tmpdir/results.json"
jq '[.results[] | {check_id, path, start: .start.line, lines: .extra.lines}]' "$tmpdir/results.json"

Repository: smj1860/fieldstay

Length of output: 6459


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

cat > "$tmpdir/supabase-error-handling-fixture.ts" <<'EOF'
async function verifyCoverage() {
  let { data } = await supabase.from('items').select()
  var { data: aliasedData } = await supabase.from('items').select()
  const { count } = await supabase.from('items').select('*', { count: 'exact' })
  let { count: aliasedCount } = await supabase.from('items').select('*', { count: 'exact' })

  let [{ data: batchData }] = await Promise.all([
    supabase.from('items').select(),
  ])
  var [{ data: varBatchData }] = await Promise.all([
    supabase.from('items').select(),
  ])
  const [{ count: batchCount }] = await Promise.all([
    supabase.from('items').select('*', { count: 'exact' }),
  ])
  let [{ count: letBatchCount }] = await Promise.all([
    supabase.from('items').select('*', { count: 'exact' }),
  ])

  let { data: handledData, error: handledError } = await supabase.from('items').select()
  const [{ count: handledCount, error: handledBatchError }] = await Promise.all([
    supabase.from('items').select('*', { count: 'exact' }),
  ])
}
EOF

printf '%s\n' '--- rules ---'
sed -n '360,455p' .semgrep/chokepoints.yml

printf '%s\n' '--- all findings ---'
semgrep --config .semgrep/chokepoints.yml --json "$tmpdir" |
  jq '[.results[] | {check_id, path, start_line: .start.line, matched: .extra.lines}]'

Repository: smj1860/fieldstay

Length of output: 6540


🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

cat > "$tmpdir/fixture.ts" <<'EOF'
async function verifyCoverage() {
  const { data } = await supabase.from('items').select()
  const { data: aliasedData } = await supabase.from('items').select()
  let { data: letData } = await supabase.from('items').select()
  var { count: varCount } = await supabase.from('items').select()

  const [{ data: batchData }] = await Promise.all([
    supabase.from('items').select(),
  ])
  let [{ data: letBatchData }] = await Promise.all([
    supabase.from('items').select(),
  ])
  var [{ count: batchCount }] = await Promise.all([
    supabase.from('items').select(),
  ])

  const { data: handledData, error: handledError } =
    await supabase.from('items').select()
  const [{ count: handledCount, error: handledBatchError }] =
    await Promise.all([supabase.from('items').select()])
}
EOF

cat > "$tmpdir/direct.yml" <<'EOF'
rules:
  - id: single
    languages: [typescript]
    message: single
    severity: ERROR
    patterns:
      - pattern-either:
          - pattern: "const { data } = await $Q"
          - pattern: "const { data: $D } = await $Q"
          - pattern: "const { data, count: $C } = await $Q"
          - pattern: "const { data: $D, count: $C } = await $Q"
      - pattern-not: "const {..., error, ...} = await $Q"
      - pattern-not: "const {..., error: $E, ...} = await $Q"
      - metavariable-pattern:
          metavariable: $Q
          pattern: <... $S.from($T) ...>
  - id: fanin
    languages: [typescript]
    message: fanin
    severity: ERROR
    patterns:
      - pattern-either:
          - pattern: "const [..., { data: $D }, ...] = await Promise.all(...)"
          - pattern: "const [..., { data }, ...] = await Promise.all(...)"
      - pattern-not: "const [..., {..., error, ...}, ...] = await Promise.all(...)"
      - pattern-not: "const [..., {..., error: $E, ...}, ...] = await Promise.all(...)"
EOF

semgrep --config "$tmpdir/direct.yml" --json "$tmpdir/fixture.ts" |
  jq '[.results[] | {check_id, start_line: .start.line, matched: .extra.lines}]'

Repository: smj1860/fieldstay

Length of output: 922


Expand both Supabase result-destructure rules.

The current rules report const data bindings but miss let and var, count-only bindings, and count aliases. Add matching patterns and error exclusions for these forms in both rules, including Promise.all fan-ins.

📍 Affects 1 file
  • .semgrep/chokepoints.yml#L398-L410 (this comment)
  • .semgrep/chokepoints.yml#L428-L432
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.semgrep/chokepoints.yml around lines 398 - 410, Expand both Supabase
result-destructure rule groups at .semgrep/chokepoints.yml lines 398-410 and
428-432 to match let and var declarations, count-only destructuring, and aliased
count bindings in addition to current const data forms. Add corresponding
error-exclusion patterns for every new declaration shape, and ensure the
expanded patterns also cover Promise.all fan-ins; preserve the existing
exclusions for destructures containing error.


# ── The same class, in a Promise.all fan-in ──────────────────────────────
# A separate rule because the metavariable-pattern that constrains the
# single-read form to a `.from()` chain cannot bind through an array
# destructure. Reported once per STATEMENT, not per element, and suppressed
# entirely when any element in the statement binds `error` — both are
# undercounts, never overcounts. unit/guardrails/supabase-error-handling.
# test.ts counts these PER ELEMENT, which is one concrete reason that test
# is not redundant with this ruleset.
- id: fieldstay-supabase-read-without-error-fan-in
languages: [typescript]
severity: ERROR
message: >-
`data` destructured without `error` in a Promise.all fan-in. Every query
in this batch that errors is indistinguishable from one that returned no
rows. Use unwrap/unwrapList from lib/supabase/unwrap.ts.
patterns:
- pattern-either:
- pattern: "const [..., { data: $D }, ...] = await Promise.all(...)"
- pattern: "const [..., { data }, ...] = await Promise.all(...)"
- pattern-not: "const [..., {..., error, ...}, ...] = await Promise.all(...)"
- pattern-not: "const [..., {..., error: $E, ...}, ...] = await Promise.all(...)"
Loading
Loading