@zveltio/sdk 3.0.0-beta.5 → 3.0.0-beta.68
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/client/Auth.d.ts.map +1 -1
- package/dist/client/Auth.js +3 -0
- package/dist/client/QueryBuilder.d.ts.map +1 -1
- package/dist/client/QueryBuilder.js +4 -0
- package/dist/client/ZveltioClient.d.ts.map +1 -1
- package/dist/client/ZveltioClient.js +5 -0
- package/dist/client.d.ts +4 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +30 -13
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +21 -3
- package/dist/crdt.d.ts.map +1 -1
- package/dist/crdt.js +11 -2
- package/dist/ddl/index.d.ts.map +1 -1
- package/dist/errors.d.ts +58 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +124 -0
- package/dist/extension/index.d.ts +419 -19
- package/dist/extension/index.d.ts.map +1 -1
- package/dist/extension/index.js +3 -0
- package/dist/extension/jsonb.d.ts +62 -0
- package/dist/extension/jsonb.d.ts.map +1 -0
- package/dist/extension/jsonb.js +64 -0
- package/dist/extension/multipart.d.ts +58 -0
- package/dist/extension/multipart.d.ts.map +1 -0
- package/dist/extension/multipart.js +61 -0
- package/dist/extension/numeric.d.ts +94 -0
- package/dist/extension/numeric.d.ts.map +1 -0
- package/dist/extension/numeric.js +162 -0
- package/dist/extension/permission-gate.d.ts +15 -0
- package/dist/extension/permission-gate.d.ts.map +1 -1
- package/dist/extension/permission-gate.js +33 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/local-store.d.ts +5 -0
- package/dist/local-store.d.ts.map +1 -1
- package/dist/local-store.js +62 -10
- package/dist/offline/index.d.ts +3 -3
- package/dist/offline/index.js +51 -15
- package/dist/publish/index.d.ts.map +1 -1
- package/dist/publish/index.js +6 -1
- package/dist/realtime.d.ts +9 -1
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js +15 -2
- package/dist/rpc/index.d.ts.map +1 -1
- package/dist/rpc/index.js +2 -0
- package/dist/schema-codegen.d.ts +3 -1
- package/dist/schema-codegen.d.ts.map +1 -1
- package/dist/schema-codegen.js +41 -7
- package/dist/schema-watcher.d.ts +1 -1
- package/dist/schema-watcher.js +1 -1
- package/dist/studio/index.d.ts +1 -0
- package/dist/studio/index.d.ts.map +1 -1
- package/dist/studio/index.js +3 -0
- package/dist/studio/slot-owner-registry.d.ts +13 -0
- package/dist/studio/slot-owner-registry.d.ts.map +1 -0
- package/dist/studio/slot-owner-registry.js +21 -0
- package/dist/svelte.d.ts +1 -1
- package/dist/svelte.d.ts.map +1 -1
- package/dist/svelte.js +4 -2
- package/dist/sync-manager.d.ts +30 -4
- package/dist/sync-manager.d.ts.map +1 -1
- package/dist/sync-manager.js +101 -15
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +23 -1
- package/dist/testing/with-test-db.d.ts.map +1 -1
- package/dist/testing/with-test-db.js +21 -14
- package/dist/tests/client.test.d.ts +2 -0
- package/dist/tests/client.test.d.ts.map +1 -0
- package/dist/tests/client.test.js +55 -0
- package/dist/tests/crdt-orset.test.d.ts +2 -0
- package/dist/tests/crdt-orset.test.d.ts.map +1 -0
- package/dist/tests/crdt-orset.test.js +23 -0
- package/dist/tests/local-store.test.js +44 -0
- package/dist/tests/multipart.test.d.ts +2 -0
- package/dist/tests/multipart.test.d.ts.map +1 -0
- package/dist/tests/multipart.test.js +37 -0
- package/dist/tests/offline-provider-crdt.test.d.ts +2 -0
- package/dist/tests/offline-provider-crdt.test.d.ts.map +1 -0
- package/dist/tests/offline-provider-crdt.test.js +119 -0
- package/dist/tests/realtime.test.d.ts +2 -0
- package/dist/tests/realtime.test.d.ts.map +1 -0
- package/dist/tests/realtime.test.js +39 -0
- package/dist/tests/sync-manager.test.d.ts +2 -0
- package/dist/tests/sync-manager.test.d.ts.map +1 -0
- package/dist/tests/sync-manager.test.js +65 -0
- package/dist/tests/with-test-db.test.d.ts +2 -0
- package/dist/tests/with-test-db.test.d.ts.map +1 -0
- package/dist/tests/with-test-db.test.js +57 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/validate/index.d.ts +49 -0
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +494 -7
- package/dist/validate/shared-message-keys.d.ts +16 -0
- package/dist/validate/shared-message-keys.d.ts.map +1 -0
- package/dist/validate/shared-message-keys.js +227 -0
- package/package.json +7 -7
- package/dist/generate-types.d.ts +0 -37
- package/dist/generate-types.d.ts.map +0 -1
- package/dist/generate-types.js +0 -157
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binding a JavaScript value to a `jsonb` column.
|
|
3
|
+
*
|
|
4
|
+
* There is one correct form and three plausible wrong ones. All four were
|
|
5
|
+
* measured against this driver, on a real `jsonb` column:
|
|
6
|
+
*
|
|
7
|
+
* JSON.stringify(v) → a jsonb STRING. `jsonb_typeof` says
|
|
8
|
+
* `string`, `col->>'k'` is NULL and
|
|
9
|
+
* `col ? 'k'` is false.
|
|
10
|
+
*
|
|
11
|
+
* the raw value → correct for an object, WRONG for an
|
|
12
|
+
* array: the driver renders a JS array
|
|
13
|
+
* as a Postgres array literal, so
|
|
14
|
+
* `[{a:1}]` lands as the string
|
|
15
|
+
* `{"[object Object]"}`.
|
|
16
|
+
*
|
|
17
|
+
* JSON.stringify(v) + `::jsonb` → still a jsonb STRING. The obvious
|
|
18
|
+
* repair and a trap: the driver has
|
|
19
|
+
* already encoded the parameter as JSON,
|
|
20
|
+
* so the cast turns a jsonb string into
|
|
21
|
+
* a jsonb string.
|
|
22
|
+
*
|
|
23
|
+
* JSON.stringify(v) + `::text::jsonb` → correct, and correct for EVERY type —
|
|
24
|
+
* object, array, string, number, boolean
|
|
25
|
+
* and null. `::text` forces the parameter
|
|
26
|
+
* to be read as text first, and the
|
|
27
|
+
* second cast parses it.
|
|
28
|
+
*
|
|
29
|
+
* ── Why this lives in the SDK ─────────────────────────────────────
|
|
30
|
+
*
|
|
31
|
+
* Because it had already been written twice. The engine has `lib/jsonb.ts`, and
|
|
32
|
+
* `content/pages` wrote its own after the defect surfaced there in the way that
|
|
33
|
+
* hurts: a popup's `targets` were appended with `jsonb ||`, and the result was
|
|
34
|
+
* an ARRAY CONTAINING THE OLD TEXT, because the old value was text. Seventeen
|
|
35
|
+
* more extensions were writing the same shape with no helper at all.
|
|
36
|
+
*
|
|
37
|
+
* A third copy per extension would be the repository's own dominant bug shape —
|
|
38
|
+
* one rule, written out again somewhere else with a piece missing — so it goes
|
|
39
|
+
* where both sides already compile against: `@zveltio/sdk/extension`.
|
|
40
|
+
*
|
|
41
|
+
* ── Why the defect is invisible until it is not ───────────────────
|
|
42
|
+
*
|
|
43
|
+
* Every reader in both repositories carries `typeof x === 'string' ?
|
|
44
|
+
* JSON.parse(x) : x`, so a string-scalar column reads back correctly and the
|
|
45
|
+
* bug hides. It surfaces the moment SQL treats the column as structured — a
|
|
46
|
+
* `->>`, an index, a `||`, a `jsonb_array_elements`. Two of those have already
|
|
47
|
+
* happened: the `content/pages` popup above, and the engine's flow scheduler,
|
|
48
|
+
* which read `trigger_config.interval_seconds` off a string and therefore ran
|
|
49
|
+
* every cron flow at the default interval instead of its configured one.
|
|
50
|
+
*
|
|
51
|
+
* `scripts/check-jsonb-binding.ts` in the engine repository is the ratchet.
|
|
52
|
+
*/
|
|
53
|
+
import { sql } from 'kysely';
|
|
54
|
+
/**
|
|
55
|
+
* A value ready to be assigned to a `jsonb` column.
|
|
56
|
+
*
|
|
57
|
+
* Works in `.values()`, in `.set()` and in a `doUpdateSet()` branch — all three
|
|
58
|
+
* are writers of the same column and all three have been wrong at some point.
|
|
59
|
+
*
|
|
60
|
+
* await db.insertInto('zv_forms').values({ fields: toJsonb(fields) }).execute();
|
|
61
|
+
*/
|
|
62
|
+
export function toJsonb(value) {
|
|
63
|
+
return sql `${JSON.stringify(value ?? null)}::text::jsonb`;
|
|
64
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a multipart body without turning a bad request into a crash.
|
|
3
|
+
*
|
|
4
|
+
* Six extensions opened an upload handler with
|
|
5
|
+
*
|
|
6
|
+
* const formData = await c.req.formData();
|
|
7
|
+
* const file = formData.get('file') as File;
|
|
8
|
+
* if (!file) return c.json({ error: 'No file provided' }, 400);
|
|
9
|
+
*
|
|
10
|
+
* The 400 on the third line says plainly what the author intended a malformed
|
|
11
|
+
* request to get. It is unreachable: `formData()` throws when the body is not
|
|
12
|
+
* multipart at all — a client that sends JSON, or sends nothing, or gets the
|
|
13
|
+
* content-type wrong — so those requests never reach line three. They surface as
|
|
14
|
+
* an unhandled rejection and a bare 500.
|
|
15
|
+
*
|
|
16
|
+
* That is worth more than tidiness. A 500 tells a caller, a retrying webhook
|
|
17
|
+
* sender, and an uptime monitor that the SERVER is broken, so the client keeps
|
|
18
|
+
* resending a request that can never succeed and the operator is paged for a
|
|
19
|
+
* fault that is not theirs. The same request through this helper gets the 400
|
|
20
|
+
* the handler already meant to send.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately not a middleware: a middleware would have to guess which routes
|
|
23
|
+
* are multipart, and being wrong in either direction is worse than one explicit
|
|
24
|
+
* call per handler.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* The body every caller sends with the 400. One string, so the six upload
|
|
28
|
+
* handlers cannot drift into six different ways of saying the same thing.
|
|
29
|
+
*/
|
|
30
|
+
export declare const MULTIPART_REQUIRED: {
|
|
31
|
+
readonly error: "Expected a multipart/form-data body.";
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Parse a multipart body, answering `null` rather than throwing when it is not one.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* const form = await readMultipart(c);
|
|
38
|
+
* if (!form) return c.json(MULTIPART_REQUIRED, 400);
|
|
39
|
+
* const file = form.get('file');
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* Returns `FormData | null` rather than a `{ ok }` result object deliberately.
|
|
43
|
+
* The extensions repo compiles with `strict: false`, and without
|
|
44
|
+
* `strictNullChecks` TypeScript will not narrow a discriminated union on a
|
|
45
|
+
* boolean member — every call site got `Property 'error' does not exist on type
|
|
46
|
+
* MultipartResult`. A nullable return needs no narrowing to be safe under either
|
|
47
|
+
* setting, and reads the same.
|
|
48
|
+
*
|
|
49
|
+
* `c` is typed structurally rather than as Hono's `Context` on purpose: the SDK
|
|
50
|
+
* has never taken a dependency on Hono, and every extension bundles its own
|
|
51
|
+
* copy, so a nominal type here would reject the caller's own context object.
|
|
52
|
+
*/
|
|
53
|
+
export declare function readMultipart(c: {
|
|
54
|
+
req: {
|
|
55
|
+
formData: () => Promise<FormData>;
|
|
56
|
+
};
|
|
57
|
+
}): Promise<FormData | null>;
|
|
58
|
+
//# sourceMappingURL=multipart.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../../src/extension/multipart.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;CAA6D,CAAC;AAE7F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,aAAa,CAAC,CAAC,EAAE;IACrC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAC;CAC5C,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAS3B"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a multipart body without turning a bad request into a crash.
|
|
3
|
+
*
|
|
4
|
+
* Six extensions opened an upload handler with
|
|
5
|
+
*
|
|
6
|
+
* const formData = await c.req.formData();
|
|
7
|
+
* const file = formData.get('file') as File;
|
|
8
|
+
* if (!file) return c.json({ error: 'No file provided' }, 400);
|
|
9
|
+
*
|
|
10
|
+
* The 400 on the third line says plainly what the author intended a malformed
|
|
11
|
+
* request to get. It is unreachable: `formData()` throws when the body is not
|
|
12
|
+
* multipart at all — a client that sends JSON, or sends nothing, or gets the
|
|
13
|
+
* content-type wrong — so those requests never reach line three. They surface as
|
|
14
|
+
* an unhandled rejection and a bare 500.
|
|
15
|
+
*
|
|
16
|
+
* That is worth more than tidiness. A 500 tells a caller, a retrying webhook
|
|
17
|
+
* sender, and an uptime monitor that the SERVER is broken, so the client keeps
|
|
18
|
+
* resending a request that can never succeed and the operator is paged for a
|
|
19
|
+
* fault that is not theirs. The same request through this helper gets the 400
|
|
20
|
+
* the handler already meant to send.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately not a middleware: a middleware would have to guess which routes
|
|
23
|
+
* are multipart, and being wrong in either direction is worse than one explicit
|
|
24
|
+
* call per handler.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* The body every caller sends with the 400. One string, so the six upload
|
|
28
|
+
* handlers cannot drift into six different ways of saying the same thing.
|
|
29
|
+
*/
|
|
30
|
+
export const MULTIPART_REQUIRED = { error: 'Expected a multipart/form-data body.' };
|
|
31
|
+
/**
|
|
32
|
+
* Parse a multipart body, answering `null` rather than throwing when it is not one.
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* const form = await readMultipart(c);
|
|
36
|
+
* if (!form) return c.json(MULTIPART_REQUIRED, 400);
|
|
37
|
+
* const file = form.get('file');
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* Returns `FormData | null` rather than a `{ ok }` result object deliberately.
|
|
41
|
+
* The extensions repo compiles with `strict: false`, and without
|
|
42
|
+
* `strictNullChecks` TypeScript will not narrow a discriminated union on a
|
|
43
|
+
* boolean member — every call site got `Property 'error' does not exist on type
|
|
44
|
+
* MultipartResult`. A nullable return needs no narrowing to be safe under either
|
|
45
|
+
* setting, and reads the same.
|
|
46
|
+
*
|
|
47
|
+
* `c` is typed structurally rather than as Hono's `Context` on purpose: the SDK
|
|
48
|
+
* has never taken a dependency on Hono, and every extension bundles its own
|
|
49
|
+
* copy, so a nominal type here would reject the caller's own context object.
|
|
50
|
+
*/
|
|
51
|
+
export async function readMultipart(c) {
|
|
52
|
+
try {
|
|
53
|
+
return await c.req.formData();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// The parser's own message is not passed through. It reads like "FormData
|
|
57
|
+
// parse error" — nothing the caller can act on — and echoing a body parser's
|
|
58
|
+
// internals back over HTTP is how request contents end up in error strings.
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning what PostgreSQL sends into what JavaScript can add.
|
|
3
|
+
*
|
|
4
|
+
* `BIGINT`, `NUMERIC`, `DECIMAL` and every `COUNT`/`SUM`/`AVG` aggregate arrive
|
|
5
|
+
* from the driver as **strings** — Bun's `SQL` exposes no type-parser hook, so
|
|
6
|
+
* there is no place to fix this once for the whole process. That leaves the call
|
|
7
|
+
* site, and the call site is where it goes wrong:
|
|
8
|
+
*
|
|
9
|
+
* total + row.amount // "0" + "12.50" → "012.50"
|
|
10
|
+
* ("012.50") - 1 // NaN
|
|
11
|
+
*
|
|
12
|
+
* Only `+` misbehaves; `-`, `*`, `/`, `<` and `>` coerce and give correct
|
|
13
|
+
* answers. So the failure hides until a sum happens to enter the chain, and then
|
|
14
|
+
* it does not throw — it produces `NaN`, PostgreSQL accepts `NaN` into a
|
|
15
|
+
* `NUMERIC` column, and `NaN` compares as LARGER than every number. A row poisoned
|
|
16
|
+
* this way passes `WHERE balance > 0`, passes `HAVING total >= required`, and
|
|
17
|
+
* passes a `CHECK (col >= 0)` constraint. The database agrees with the corruption.
|
|
18
|
+
*
|
|
19
|
+
* Hence two rules, both enforced here rather than remembered:
|
|
20
|
+
*
|
|
21
|
+
* * `toNumber` REFUSES `NaN` and `Infinity`. Not "returns 0" — refuses. A value
|
|
22
|
+
* that reached this point non-finite is already the product of a bug, and
|
|
23
|
+
* substituting a plausible number buries it one layer deeper.
|
|
24
|
+
* * `sumNumeric` exists so the common case — adding a column across rows — has
|
|
25
|
+
* an obvious right answer that is shorter to write than the wrong one.
|
|
26
|
+
*
|
|
27
|
+
* This lives in the SDK, not the engine, on purpose. The safe helper existing
|
|
28
|
+
* somewhere an extension cannot import it is a shape this codebase has produced
|
|
29
|
+
* four times already — `safeFetch`, `maybeDecrypt`, `requireInstanceAdmin` and
|
|
30
|
+
* the validation evaluator all had an engine-side implementation and an
|
|
31
|
+
* extension-side reimplementation that was subtly worse. Every module that
|
|
32
|
+
* handles money is an extension, so the conversion belongs where they can reach
|
|
33
|
+
* it: `import { toNumber } from '@zveltio/sdk/extension'`.
|
|
34
|
+
*
|
|
35
|
+
* The engine keeps its own copy at `lib/numeric.ts` rather than importing this
|
|
36
|
+
* one, because the engine's production code has never taken a RUNTIME dependency
|
|
37
|
+
* on the SDK — only types — and a pure sixty-line converter is not the thing to
|
|
38
|
+
* change that for. `numeric-parity.test.ts` runs both against the same table of
|
|
39
|
+
* inputs so the twins cannot drift apart quietly.
|
|
40
|
+
*
|
|
41
|
+
* @see `PgNumeric` in the engine's `db/schema.ts` for the type side of the same problem.
|
|
42
|
+
*/
|
|
43
|
+
/** Thrown instead of quietly producing a number nobody can trace. */
|
|
44
|
+
export declare class NumericConversionError extends Error {
|
|
45
|
+
readonly value: unknown;
|
|
46
|
+
constructor(value: unknown, label?: string);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A database numeric as a JS number, or a throw.
|
|
50
|
+
*
|
|
51
|
+
* Accepts what the driver actually produces: a string, a number that survived
|
|
52
|
+
* because the column was `INTEGER`, a `bigint`, or `null`/`undefined` for a
|
|
53
|
+
* nullable column. Everything else, and anything non-finite, is an error.
|
|
54
|
+
*
|
|
55
|
+
* `null` and `undefined` map to `fallback`, which defaults to `0`. That is the
|
|
56
|
+
* one substitution worth making: "no row" and "no value" genuinely do mean zero
|
|
57
|
+
* in an aggregate, and the alternative is a `?? 0` at every call site, which is
|
|
58
|
+
* the shape that lets a real `NaN` through.
|
|
59
|
+
*/
|
|
60
|
+
export declare function toNumber(value: unknown, fallback?: number, label?: string): number;
|
|
61
|
+
/**
|
|
62
|
+
* `toNumber` for a value that is allowed to be absent, with no substitution.
|
|
63
|
+
*
|
|
64
|
+
* Use it where zero and "not set" mean different things — a quota that has not
|
|
65
|
+
* been configured is not a quota of zero.
|
|
66
|
+
*/
|
|
67
|
+
export declare function toNumberOrNull(value: unknown, label?: string): number | null;
|
|
68
|
+
/**
|
|
69
|
+
* `toNumber` that answers `fallback` instead of throwing.
|
|
70
|
+
*
|
|
71
|
+
* For display paths only — a dashboard tile should not 500 because one row is
|
|
72
|
+
* bad. Never use it on a value about to be written back to the database, which
|
|
73
|
+
* is exactly how the poison spreads.
|
|
74
|
+
*/
|
|
75
|
+
export declare function toNumberSafe(value: unknown, fallback?: number): number;
|
|
76
|
+
/** Sum one numeric column across rows, without a `+` on a string in sight. */
|
|
77
|
+
export declare function sumNumeric<T>(rows: readonly T[], pick: (row: T) => unknown, label?: string): number;
|
|
78
|
+
/**
|
|
79
|
+
* Round to a currency's minor unit.
|
|
80
|
+
*
|
|
81
|
+
* Money read as a string and converted to a double is exact for every value a
|
|
82
|
+
* `NUMERIC(14,2)` can hold, but arithmetic on doubles is not: `0.1 + 0.2` is
|
|
83
|
+
* `0.30000000000000004`, and written back that is a `NUMERIC` with fourteen
|
|
84
|
+
* decimal places. Round at the point the result becomes an amount again.
|
|
85
|
+
*/
|
|
86
|
+
export declare function roundMoney(value: number, decimals?: number): number;
|
|
87
|
+
/**
|
|
88
|
+
* Is this value safe to store in a `NUMERIC` column?
|
|
89
|
+
*
|
|
90
|
+
* The database will not tell you. `INSERT ... VALUES ('NaN'::numeric)` succeeds,
|
|
91
|
+
* and so does a `CHECK (col >= 0)` on the row afterwards. Guard on the way in.
|
|
92
|
+
*/
|
|
93
|
+
export declare function isStorableNumeric(value: unknown): boolean;
|
|
94
|
+
//# sourceMappingURL=numeric.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"numeric.d.ts","sourceRoot":"","sources":["../../src/extension/numeric.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,qEAAqE;AACrE,qBAAa,sBAAuB,SAAQ,KAAK;IAE7C,QAAQ,CAAC,KAAK,EAAE,OAAO;gBAAd,KAAK,EAAE,OAAO,EACvB,KAAK,CAAC,EAAE,MAAM;CASjB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,SAAI,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAwB7E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG5E;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,SAAI,GAAG,MAAM,CAMjE;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,CAAC,EAC1B,IAAI,EAAE,SAAS,CAAC,EAAE,EAClB,IAAI,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,EACzB,KAAK,CAAC,EAAE,MAAM,GACb,MAAM,CAIR;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAU9D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQzD"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning what PostgreSQL sends into what JavaScript can add.
|
|
3
|
+
*
|
|
4
|
+
* `BIGINT`, `NUMERIC`, `DECIMAL` and every `COUNT`/`SUM`/`AVG` aggregate arrive
|
|
5
|
+
* from the driver as **strings** — Bun's `SQL` exposes no type-parser hook, so
|
|
6
|
+
* there is no place to fix this once for the whole process. That leaves the call
|
|
7
|
+
* site, and the call site is where it goes wrong:
|
|
8
|
+
*
|
|
9
|
+
* total + row.amount // "0" + "12.50" → "012.50"
|
|
10
|
+
* ("012.50") - 1 // NaN
|
|
11
|
+
*
|
|
12
|
+
* Only `+` misbehaves; `-`, `*`, `/`, `<` and `>` coerce and give correct
|
|
13
|
+
* answers. So the failure hides until a sum happens to enter the chain, and then
|
|
14
|
+
* it does not throw — it produces `NaN`, PostgreSQL accepts `NaN` into a
|
|
15
|
+
* `NUMERIC` column, and `NaN` compares as LARGER than every number. A row poisoned
|
|
16
|
+
* this way passes `WHERE balance > 0`, passes `HAVING total >= required`, and
|
|
17
|
+
* passes a `CHECK (col >= 0)` constraint. The database agrees with the corruption.
|
|
18
|
+
*
|
|
19
|
+
* Hence two rules, both enforced here rather than remembered:
|
|
20
|
+
*
|
|
21
|
+
* * `toNumber` REFUSES `NaN` and `Infinity`. Not "returns 0" — refuses. A value
|
|
22
|
+
* that reached this point non-finite is already the product of a bug, and
|
|
23
|
+
* substituting a plausible number buries it one layer deeper.
|
|
24
|
+
* * `sumNumeric` exists so the common case — adding a column across rows — has
|
|
25
|
+
* an obvious right answer that is shorter to write than the wrong one.
|
|
26
|
+
*
|
|
27
|
+
* This lives in the SDK, not the engine, on purpose. The safe helper existing
|
|
28
|
+
* somewhere an extension cannot import it is a shape this codebase has produced
|
|
29
|
+
* four times already — `safeFetch`, `maybeDecrypt`, `requireInstanceAdmin` and
|
|
30
|
+
* the validation evaluator all had an engine-side implementation and an
|
|
31
|
+
* extension-side reimplementation that was subtly worse. Every module that
|
|
32
|
+
* handles money is an extension, so the conversion belongs where they can reach
|
|
33
|
+
* it: `import { toNumber } from '@zveltio/sdk/extension'`.
|
|
34
|
+
*
|
|
35
|
+
* The engine keeps its own copy at `lib/numeric.ts` rather than importing this
|
|
36
|
+
* one, because the engine's production code has never taken a RUNTIME dependency
|
|
37
|
+
* on the SDK — only types — and a pure sixty-line converter is not the thing to
|
|
38
|
+
* change that for. `numeric-parity.test.ts` runs both against the same table of
|
|
39
|
+
* inputs so the twins cannot drift apart quietly.
|
|
40
|
+
*
|
|
41
|
+
* @see `PgNumeric` in the engine's `db/schema.ts` for the type side of the same problem.
|
|
42
|
+
*/
|
|
43
|
+
/** Thrown instead of quietly producing a number nobody can trace. */
|
|
44
|
+
export class NumericConversionError extends Error {
|
|
45
|
+
value;
|
|
46
|
+
constructor(value, label) {
|
|
47
|
+
super(`${label ? `${label}: ` : ''}expected a finite number, got ${typeof value === 'string' ? JSON.stringify(value) : String(value)}`);
|
|
48
|
+
this.value = value;
|
|
49
|
+
this.name = 'NumericConversionError';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A database numeric as a JS number, or a throw.
|
|
54
|
+
*
|
|
55
|
+
* Accepts what the driver actually produces: a string, a number that survived
|
|
56
|
+
* because the column was `INTEGER`, a `bigint`, or `null`/`undefined` for a
|
|
57
|
+
* nullable column. Everything else, and anything non-finite, is an error.
|
|
58
|
+
*
|
|
59
|
+
* `null` and `undefined` map to `fallback`, which defaults to `0`. That is the
|
|
60
|
+
* one substitution worth making: "no row" and "no value" genuinely do mean zero
|
|
61
|
+
* in an aggregate, and the alternative is a `?? 0` at every call site, which is
|
|
62
|
+
* the shape that lets a real `NaN` through.
|
|
63
|
+
*/
|
|
64
|
+
export function toNumber(value, fallback = 0, label) {
|
|
65
|
+
if (value === null || value === undefined)
|
|
66
|
+
return fallback;
|
|
67
|
+
if (typeof value === 'bigint') {
|
|
68
|
+
// Above 2^53 a bigint cannot round-trip through a double. Refusing beats
|
|
69
|
+
// returning a number that is quietly off by a few.
|
|
70
|
+
if (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER) {
|
|
71
|
+
throw new NumericConversionError(value, label);
|
|
72
|
+
}
|
|
73
|
+
return Number(value);
|
|
74
|
+
}
|
|
75
|
+
if (typeof value === 'number') {
|
|
76
|
+
if (!Number.isFinite(value))
|
|
77
|
+
throw new NumericConversionError(value, label);
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
if (typeof value === 'string') {
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
// `Number('')` is 0 and `Number(' ')` is 0 — both of which would turn a
|
|
83
|
+
// malformed column into a confident zero.
|
|
84
|
+
if (trimmed === '')
|
|
85
|
+
throw new NumericConversionError(value, label);
|
|
86
|
+
const n = Number(trimmed);
|
|
87
|
+
if (!Number.isFinite(n))
|
|
88
|
+
throw new NumericConversionError(value, label);
|
|
89
|
+
return n;
|
|
90
|
+
}
|
|
91
|
+
throw new NumericConversionError(value, label);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* `toNumber` for a value that is allowed to be absent, with no substitution.
|
|
95
|
+
*
|
|
96
|
+
* Use it where zero and "not set" mean different things — a quota that has not
|
|
97
|
+
* been configured is not a quota of zero.
|
|
98
|
+
*/
|
|
99
|
+
export function toNumberOrNull(value, label) {
|
|
100
|
+
if (value === null || value === undefined)
|
|
101
|
+
return null;
|
|
102
|
+
return toNumber(value, 0, label);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* `toNumber` that answers `fallback` instead of throwing.
|
|
106
|
+
*
|
|
107
|
+
* For display paths only — a dashboard tile should not 500 because one row is
|
|
108
|
+
* bad. Never use it on a value about to be written back to the database, which
|
|
109
|
+
* is exactly how the poison spreads.
|
|
110
|
+
*/
|
|
111
|
+
export function toNumberSafe(value, fallback = 0) {
|
|
112
|
+
try {
|
|
113
|
+
return toNumber(value, fallback);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return fallback;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Sum one numeric column across rows, without a `+` on a string in sight. */
|
|
120
|
+
export function sumNumeric(rows, pick, label) {
|
|
121
|
+
let total = 0;
|
|
122
|
+
for (const row of rows)
|
|
123
|
+
total += toNumber(pick(row), 0, label);
|
|
124
|
+
return total;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Round to a currency's minor unit.
|
|
128
|
+
*
|
|
129
|
+
* Money read as a string and converted to a double is exact for every value a
|
|
130
|
+
* `NUMERIC(14,2)` can hold, but arithmetic on doubles is not: `0.1 + 0.2` is
|
|
131
|
+
* `0.30000000000000004`, and written back that is a `NUMERIC` with fourteen
|
|
132
|
+
* decimal places. Round at the point the result becomes an amount again.
|
|
133
|
+
*/
|
|
134
|
+
export function roundMoney(value, decimals = 2) {
|
|
135
|
+
if (!Number.isFinite(value))
|
|
136
|
+
throw new NumericConversionError(value, 'roundMoney');
|
|
137
|
+
const factor = 10 ** decimals;
|
|
138
|
+
// Round the MAGNITUDE, then restore the sign. `Math.round` breaks ties
|
|
139
|
+
// towards +Infinity, so a plain `Math.round(v * factor)` rounds 2.675 up to
|
|
140
|
+
// 2.68 and -2.675 up to -2.67 — a credit note and the charge it reverses
|
|
141
|
+
// stop cancelling, and `finance/accounting` computes its balance-sheet
|
|
142
|
+
// difference from exactly that subtraction. Money rounds away from zero.
|
|
143
|
+
const sign = value < 0 ? -1 : 1;
|
|
144
|
+
return (sign * Math.round((Math.abs(value) + Number.EPSILON) * factor)) / factor;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Is this value safe to store in a `NUMERIC` column?
|
|
148
|
+
*
|
|
149
|
+
* The database will not tell you. `INSERT ... VALUES ('NaN'::numeric)` succeeds,
|
|
150
|
+
* and so does a `CHECK (col >= 0)` on the row afterwards. Guard on the way in.
|
|
151
|
+
*/
|
|
152
|
+
export function isStorableNumeric(value) {
|
|
153
|
+
if (value === null || value === undefined)
|
|
154
|
+
return true;
|
|
155
|
+
try {
|
|
156
|
+
toNumber(value);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -27,8 +27,23 @@
|
|
|
27
27
|
* Place AFTER the auth-guard middleware that sets `c.set('user', ...)`.
|
|
28
28
|
*/
|
|
29
29
|
type MaybePromise<T> = T | Promise<T>;
|
|
30
|
+
interface Granter {
|
|
31
|
+
name: string;
|
|
32
|
+
}
|
|
33
|
+
interface DenialContext {
|
|
34
|
+
resource: string;
|
|
35
|
+
action: string;
|
|
36
|
+
confidential: boolean;
|
|
37
|
+
canGrant: Granter[];
|
|
38
|
+
}
|
|
30
39
|
interface GateContext {
|
|
31
40
|
checkPermission: (userId: string, resource: string, action: string) => Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Supplied by the host so a refusal can say who to ask. Optional because an
|
|
43
|
+
* extension may be running against an engine older than this, and a missing
|
|
44
|
+
* courtesy must never become a missing refusal.
|
|
45
|
+
*/
|
|
46
|
+
describeDenial?: (resource: string, action: string) => Promise<DenialContext>;
|
|
32
47
|
}
|
|
33
48
|
export declare function permissionGate(ctx: GateContext, resource: string, opts?: {
|
|
34
49
|
actionOverrides?: Record<string, string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permission-gate.d.ts","sourceRoot":"","sources":["../../src/extension/permission-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEtC,UAAU,WAAW;IACnB,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"permission-gate.d.ts","sourceRoot":"","sources":["../../src/extension/permission-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEtC,UAAU,OAAO;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,aAAa;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,UAAU,WAAW;IACnB,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACxF;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;CAC/E;AAmGD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,WAAW,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAO,IAGzC,GAAG,GAAG,EAAE,MAAM,MAAM,YAAY,CAAC,OAAO,CAAC,kBAgCxD"}
|
|
@@ -26,6 +26,36 @@
|
|
|
26
26
|
*
|
|
27
27
|
* Place AFTER the auth-guard middleware that sets `c.set('user', ...)`.
|
|
28
28
|
*/
|
|
29
|
+
async function refuse(ctx, c, resource, action) {
|
|
30
|
+
let denial = null;
|
|
31
|
+
try {
|
|
32
|
+
denial = (await ctx.describeDenial?.(resource, action)) ?? null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* the refusal matters more than the suggestion */
|
|
36
|
+
}
|
|
37
|
+
const names = denial?.canGrant.map((g) => g.name) ?? [];
|
|
38
|
+
const who = names.length === 0
|
|
39
|
+
? 'An administrator of this workspace'
|
|
40
|
+
: names.length === 1
|
|
41
|
+
? names[0]
|
|
42
|
+
: `${names.slice(0, -1).join(', ')} or ${names[names.length - 1]}`;
|
|
43
|
+
const what = denial?.confidential
|
|
44
|
+
? `${resource} is confidential`
|
|
45
|
+
: `you do not have access to ${resource}`;
|
|
46
|
+
const detail = names.length === 0 ? `${what}. ${who} can grant it.` : `${what}. ${who} can give you access.`;
|
|
47
|
+
return c.json({
|
|
48
|
+
type: 'about:blank',
|
|
49
|
+
title: 'Forbidden',
|
|
50
|
+
status: 403,
|
|
51
|
+
code: 'permission_required',
|
|
52
|
+
detail,
|
|
53
|
+
resource,
|
|
54
|
+
action,
|
|
55
|
+
confidential: denial?.confidential ?? false,
|
|
56
|
+
can_grant: names.map((name) => ({ name })),
|
|
57
|
+
}, 403);
|
|
58
|
+
}
|
|
29
59
|
function methodToAction(method) {
|
|
30
60
|
const m = method.toUpperCase();
|
|
31
61
|
switch (m) {
|
|
@@ -56,10 +86,12 @@ function methodToAction(method) {
|
|
|
56
86
|
* Strict (default) is the secure posture and what the SDK ships with.
|
|
57
87
|
*/
|
|
58
88
|
function rbacMode() {
|
|
89
|
+
// biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
|
|
59
90
|
const v = globalThis.process?.env?.EXTENSION_RBAC;
|
|
60
91
|
return v === 'permissive' ? 'permissive' : 'strict';
|
|
61
92
|
}
|
|
62
93
|
export function permissionGate(ctx, resource, opts = {}) {
|
|
94
|
+
// biome-ignore lint/suspicious/noExplicitAny: legacy any; tracked in hardening plan item H-01
|
|
63
95
|
return async (c, next) => {
|
|
64
96
|
// CORS preflight (OPTIONS) must never be authorization-gated —
|
|
65
97
|
// browsers send it without credentials and a 401/403 here breaks
|
|
@@ -86,7 +118,7 @@ export function permissionGate(ctx, resource, opts = {}) {
|
|
|
86
118
|
await next();
|
|
87
119
|
return;
|
|
88
120
|
}
|
|
89
|
-
return
|
|
121
|
+
return refuse(ctx, c, resource, action);
|
|
90
122
|
}
|
|
91
123
|
await next();
|
|
92
124
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { ZveltioClient, createZveltioClient } from './client.js';
|
|
2
2
|
export type { ZveltioClientConfig, ListParams, ListResult } from './client.js';
|
|
3
|
+
export { ZveltioApiError, PROBLEM_CONTENT_TYPE } from './errors.js';
|
|
4
|
+
export type { ProblemDetails } from './errors.js';
|
|
3
5
|
export { ZveltioRealtime } from './realtime.js';
|
|
4
6
|
export { LocalStore } from './local-store.js';
|
|
5
7
|
export { SyncManager } from './sync-manager.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EAAE,mBAAmB,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACnE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC/D,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAG1B,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACjE,YAAY,EAAE,mBAAmB,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACnE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC/D,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAG1B,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ZveltioClient, createZveltioClient } from './client.js';
|
|
2
|
+
export { ZveltioApiError, PROBLEM_CONTENT_TYPE } from './errors.js';
|
|
2
3
|
export { ZveltioRealtime } from './realtime.js';
|
|
3
4
|
export { LocalStore } from './local-store.js';
|
|
4
5
|
export { SyncManager } from './sync-manager.js';
|
package/dist/local-store.d.ts
CHANGED
|
@@ -51,6 +51,11 @@ export declare class LocalStore {
|
|
|
51
51
|
markFailed(queueItemId: string, error: string): Promise<void>;
|
|
52
52
|
/** Apply data from server (via WebSocket or pull) */
|
|
53
53
|
applyServerUpdate(collection: string, id: string, data: Record<string, any>, serverVersion: number): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* The server deleted a record: drop the local copy WITHOUT queueing a delete.
|
|
56
|
+
* `delete()` would queue one, echoing the server's own deletion back at it.
|
|
57
|
+
*/
|
|
58
|
+
applyServerDelete(collection: string, id: string): Promise<void>;
|
|
54
59
|
/** Get records with conflicts for UI resolution */
|
|
55
60
|
getConflicts(collection?: string): Promise<LocalRecord[]>;
|
|
56
61
|
/** Resolve a conflict (user decides which version wins) */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-store.d.ts","sourceRoot":"","sources":["../src/local-store.ts"],"names":[],"mappings":"AACA,OAAO,EAAoD,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAG/F,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"local-store.d.ts","sourceRoot":"","sources":["../src/local-store.ts"],"names":[],"mappings":"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"}
|