naro-js 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +334 -0
- package/dist/auth/client.d.ts +122 -0
- package/dist/auth/flow-errors.d.ts +4 -0
- package/dist/auth/flows.d.ts +17 -0
- package/dist/auth/pkce.d.ts +10 -0
- package/dist/auth/recovery.d.ts +15 -0
- package/dist/auth/storage.d.ts +61 -0
- package/dist/client.d.ts +61 -0
- package/dist/errors.d.ts +51 -0
- package/dist/http.d.ts +61 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1733 -0
- package/dist/query/builder.d.ts +211 -0
- package/dist/query/columns.d.ts +15 -0
- package/dist/query/filters.d.ts +63 -0
- package/dist/query/selected.d.ts +35 -0
- package/dist/runtime.d.ts +13 -0
- package/dist/types.d.ts +186 -0
- package/dist/wire.d.ts +61 -0
- package/package.json +37 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { type NaroError } from "../errors.js";
|
|
2
|
+
import type { ColumnName, FilterValue, NaroResponse, OrderOptions } from "../types.js";
|
|
3
|
+
import type { FilterNode, Order, Query, Returning, Row as WireRow } from "../wire.js";
|
|
4
|
+
import { type FilterOperatorName } from "./filters.js";
|
|
5
|
+
import type { ColumnList, Selected } from "./selected.js";
|
|
6
|
+
/**
|
|
7
|
+
* Spec §9: the most rows one select reads, whether by `limit` or `range`.
|
|
8
|
+
* data-core's MAX_LIMIT; `wire.test.ts` checks that the two agree.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_LIMIT = 1000;
|
|
11
|
+
/** Sends a finished AST and reads `{ data }` or `{ error }` (see client.ts). */
|
|
12
|
+
export type Runner = (query: Query) => Promise<NaroResponse<unknown>>;
|
|
13
|
+
/**
|
|
14
|
+
* Everything a chain has said so far. Builders never change it: each call
|
|
15
|
+
* returns a new builder holding a new state, so a builder kept in a variable
|
|
16
|
+
* and extended twice gives two independent queries (spec §15: thenable
|
|
17
|
+
* builders; reuse must not leak filters).
|
|
18
|
+
*/
|
|
19
|
+
type State = {
|
|
20
|
+
table: string;
|
|
21
|
+
/** The first mistake the builder caught; the query is answered with it. */
|
|
22
|
+
problem: NaroError | null;
|
|
23
|
+
filters: readonly FilterNode[];
|
|
24
|
+
columns: string[] | "*";
|
|
25
|
+
order: readonly Order[];
|
|
26
|
+
limit?: number;
|
|
27
|
+
offset?: number;
|
|
28
|
+
single?: "one" | "maybe";
|
|
29
|
+
action?: "update" | "delete";
|
|
30
|
+
rows?: WireRow[];
|
|
31
|
+
values?: WireRow;
|
|
32
|
+
returning?: Returning;
|
|
33
|
+
allowUnfiltered?: true;
|
|
34
|
+
};
|
|
35
|
+
declare abstract class Builder<Result> implements PromiseLike<NaroResponse<Result>> {
|
|
36
|
+
protected readonly run: Runner;
|
|
37
|
+
protected readonly state: State;
|
|
38
|
+
constructor(run: Runner, state: State);
|
|
39
|
+
protected abstract build(): Query;
|
|
40
|
+
/** A new builder of the same kind with `patch` applied. */
|
|
41
|
+
protected next(patch: Partial<State>): this;
|
|
42
|
+
protected fail(path: string, reason: string): this;
|
|
43
|
+
/** Keeps the first problem: it is the one the user has to fix first. */
|
|
44
|
+
protected failWith(problem: NaroError): this;
|
|
45
|
+
/**
|
|
46
|
+
* Runs the query. Every `await` sends a new request with the same AST.
|
|
47
|
+
* Never rejects: failures resolve as `{ data: null, error }`.
|
|
48
|
+
*/
|
|
49
|
+
then<Fulfilled = NaroResponse<Result>, Rejected = never>(onfulfilled?: ((value: NaroResponse<Result>) => Fulfilled | PromiseLike<Fulfilled>) | null, onrejected?: ((reason: unknown) => Rejected | PromiseLike<Rejected>) | null): Promise<Fulfilled | Rejected>;
|
|
50
|
+
private execute;
|
|
51
|
+
/** A mutation's `.select()`: RETURNING these columns. */
|
|
52
|
+
protected withReturning(columns: string | undefined): this;
|
|
53
|
+
}
|
|
54
|
+
/** What `.or()`, `.and()` and `.not()` hand a callback, and want back. */
|
|
55
|
+
export type FilterGroupCallback<Row> = (group: FilterGroup<Row>) => FilterGroup<Row>;
|
|
56
|
+
/**
|
|
57
|
+
* The filter methods, on every select, update and delete — and on the `q` of
|
|
58
|
+
* `.or((q) => …)`, `.and((q) => …)` and `.not((q) => …)`, where the
|
|
59
|
+
* conditions `q` collects become the group's (Phase 2-1, spec P2-1 §4).
|
|
60
|
+
*/
|
|
61
|
+
export interface FilterGroup<Row> {
|
|
62
|
+
eq(column: ColumnName<Row>, value: FilterValue): FilterGroup<Row>;
|
|
63
|
+
neq(column: ColumnName<Row>, value: FilterValue): FilterGroup<Row>;
|
|
64
|
+
gt(column: ColumnName<Row>, value: FilterValue): FilterGroup<Row>;
|
|
65
|
+
gte(column: ColumnName<Row>, value: FilterValue): FilterGroup<Row>;
|
|
66
|
+
lt(column: ColumnName<Row>, value: FilterValue): FilterGroup<Row>;
|
|
67
|
+
lte(column: ColumnName<Row>, value: FilterValue): FilterGroup<Row>;
|
|
68
|
+
in(column: ColumnName<Row>, values: readonly FilterValue[]): FilterGroup<Row>;
|
|
69
|
+
is(column: ColumnName<Row>, value: boolean | null): FilterGroup<Row>;
|
|
70
|
+
like(column: ColumnName<Row>, pattern: string): FilterGroup<Row>;
|
|
71
|
+
ilike(column: ColumnName<Row>, pattern: string): FilterGroup<Row>;
|
|
72
|
+
contains(column: ColumnName<Row>, values: ArrayValues): FilterGroup<Row>;
|
|
73
|
+
containedBy(column: ColumnName<Row>, values: ArrayValues): FilterGroup<Row>;
|
|
74
|
+
overlaps(column: ColumnName<Row>, values: ArrayValues): FilterGroup<Row>;
|
|
75
|
+
match(query: MatchQuery<Row>): FilterGroup<Row>;
|
|
76
|
+
filter(column: ColumnName<Row>, operator: FilterOperatorName | `not.${FilterOperatorName}`, value: RawFilterValue): FilterGroup<Row>;
|
|
77
|
+
not(column: ColumnName<Row>, operator: FilterOperatorName, value: RawFilterValue): FilterGroup<Row>;
|
|
78
|
+
not(group: FilterGroupCallback<Row>): FilterGroup<Row>;
|
|
79
|
+
or(filters: string | FilterGroupCallback<Row>): FilterGroup<Row>;
|
|
80
|
+
and(filters: string | FilterGroupCallback<Row>): FilterGroup<Row>;
|
|
81
|
+
}
|
|
82
|
+
/** What `contains`, `containedBy` and `overlaps` compare with: strings and numbers. */
|
|
83
|
+
export type ArrayValues = readonly (string | number)[];
|
|
84
|
+
/** `.match({ column: value, … })`: an `eq` for each. */
|
|
85
|
+
export type MatchQuery<Row> = {
|
|
86
|
+
[Column in ColumnName<Row>]?: FilterValue;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* A value for `.filter()` and `.not()`: a value or a list, or a string in
|
|
90
|
+
* PostgREST's syntax where the operator needs one (`"(1,2)"` for `in`,
|
|
91
|
+
* `"{a,b}"` for `cs`, `"null"` for `is`).
|
|
92
|
+
*/
|
|
93
|
+
export type RawFilterValue = FilterValue | readonly FilterValue[];
|
|
94
|
+
declare abstract class FilterBuilder<Row, Result> extends Builder<Result> implements FilterGroup<Row> {
|
|
95
|
+
/** One more entry in the top-level list, which is an AND. */
|
|
96
|
+
private add;
|
|
97
|
+
private condition;
|
|
98
|
+
eq(column: ColumnName<Row>, value: FilterValue): this;
|
|
99
|
+
neq(column: ColumnName<Row>, value: FilterValue): this;
|
|
100
|
+
gt(column: ColumnName<Row>, value: FilterValue): this;
|
|
101
|
+
gte(column: ColumnName<Row>, value: FilterValue): this;
|
|
102
|
+
lt(column: ColumnName<Row>, value: FilterValue): this;
|
|
103
|
+
lte(column: ColumnName<Row>, value: FilterValue): this;
|
|
104
|
+
/** An empty list matches nothing. */
|
|
105
|
+
in(column: ColumnName<Row>, values: readonly FilterValue[]): this;
|
|
106
|
+
/** `IS NULL`, `IS TRUE` or `IS FALSE` — any non-zero number is true, NULL neither. */
|
|
107
|
+
is(column: ColumnName<Row>, value: boolean | null): this;
|
|
108
|
+
/**
|
|
109
|
+
* Case-sensitive, `%` and `_` as in SQL; `*` is `%` too, as in PostgREST,
|
|
110
|
+
* so a literal `*` cannot be matched.
|
|
111
|
+
*/
|
|
112
|
+
like(column: ColumnName<Row>, pattern: string): this;
|
|
113
|
+
/** Case-insensitive for ASCII letters; `%`, `_` and `*` as in `like`. */
|
|
114
|
+
ilike(column: ColumnName<Row>, pattern: string): this;
|
|
115
|
+
/**
|
|
116
|
+
* The column — TEXT holding a JSON array — has every one of the values.
|
|
117
|
+
* A row that is not a JSON array is no match; NULL is neither.
|
|
118
|
+
*/
|
|
119
|
+
contains(column: ColumnName<Row>, values: ArrayValues): this;
|
|
120
|
+
/** Every element of the column's JSON array is one of the values (`[]` is). */
|
|
121
|
+
containedBy(column: ColumnName<Row>, values: ArrayValues): this;
|
|
122
|
+
/** The column's JSON array has at least one of the values. */
|
|
123
|
+
overlaps(column: ColumnName<Row>, values: ArrayValues): this;
|
|
124
|
+
/** An `eq` for each key, in key order. */
|
|
125
|
+
match(query: MatchQuery<Row>): this;
|
|
126
|
+
/**
|
|
127
|
+
* Any operator by name, Supabase's escape hatch: `filter("id", "in",
|
|
128
|
+
* "(1,2)")`, `filter("tags", "cs", "{a}")`, `filter("n", "not.gte", 10)`.
|
|
129
|
+
* A string value is PostgREST syntax where the operator needs one, and text
|
|
130
|
+
* otherwise.
|
|
131
|
+
*/
|
|
132
|
+
filter(column: ColumnName<Row>, operator: FilterOperatorName | `not.${FilterOperatorName}`, value: RawFilterValue): this;
|
|
133
|
+
/**
|
|
134
|
+
* NOT of one condition — `not("deleted_at", "is", null)`, with the
|
|
135
|
+
* arguments `.filter()` takes — or of the conditions a callback adds, ANDed.
|
|
136
|
+
* As in SQL and PostgREST, a row whose column is NULL matches neither a
|
|
137
|
+
* condition nor its `not`: `not("status", "eq", "draft")` leaves NULL out.
|
|
138
|
+
*/
|
|
139
|
+
not(column: ColumnName<Row>, operator: FilterOperatorName, value: RawFilterValue): this;
|
|
140
|
+
not(group: FilterGroupCallback<Row>): this;
|
|
141
|
+
/**
|
|
142
|
+
* Rows matching at least one of the conditions: a callback,
|
|
143
|
+
* `or((q) => q.eq("status", "draft").lt("price", 10))`, or Supabase's
|
|
144
|
+
* string, `or("status.eq.draft,price.lt.10")`, whose values are text.
|
|
145
|
+
*/
|
|
146
|
+
or(filters: string | FilterGroupCallback<Row>): this;
|
|
147
|
+
/** Rows matching all of the conditions, as a group: for use inside `or`. */
|
|
148
|
+
and(filters: string | FilterGroupCallback<Row>): this;
|
|
149
|
+
private logic;
|
|
150
|
+
/**
|
|
151
|
+
* The conditions `build` adds, or why there are none to use. `build` gets
|
|
152
|
+
* a builder of this kind with no filters; FilterGroup shows only its filter
|
|
153
|
+
* methods, and only its conditions and first problem are read back.
|
|
154
|
+
*/
|
|
155
|
+
private group;
|
|
156
|
+
protected filtersPart(): {
|
|
157
|
+
filters?: FilterNode[];
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* `naro.from(table).select(…)` — resolves to the rows, or one row. `Row` is
|
|
162
|
+
* the table's (what filters and `order` name), `Picked` the columns chosen.
|
|
163
|
+
*/
|
|
164
|
+
export declare class SelectBuilder<Row, Result = Row[], Picked = Row> extends FilterBuilder<Row, Result> {
|
|
165
|
+
/** Call again to sort by more columns, in call order. */
|
|
166
|
+
order(column: ColumnName<Row>, options?: OrderOptions): this;
|
|
167
|
+
/** At most `count` rows (1–1,000; a select without one reads up to 1,000). */
|
|
168
|
+
limit(count: number): this;
|
|
169
|
+
offset(count: number): this;
|
|
170
|
+
/**
|
|
171
|
+
* Rows `from` to `to`, both inclusive and counted from 0 — at most 1,000
|
|
172
|
+
* of them. Sent as `offset` and `limit`, so it is checked here: past the
|
|
173
|
+
* cap the worker would name `limit`, which the app never called.
|
|
174
|
+
*/
|
|
175
|
+
range(from: number, to: number): this;
|
|
176
|
+
/** Exactly one row, or NOT_SINGLE_ROW. */
|
|
177
|
+
single(): SelectBuilder<Row, Picked, Picked>;
|
|
178
|
+
/** One row or null; more than one is NOT_SINGLE_ROW. */
|
|
179
|
+
maybeSingle(): SelectBuilder<Row, Picked | null, Picked>;
|
|
180
|
+
protected build(): Query;
|
|
181
|
+
}
|
|
182
|
+
/** `naro.from(table).insert(…)` — resolves to null, or the rows with `.select()`. */
|
|
183
|
+
export declare class InsertBuilder<Row, Result = null> extends Builder<Result> {
|
|
184
|
+
/** Return the inserted rows (all columns, or the ones named). */
|
|
185
|
+
select<Columns extends string = "*">(columns?: ColumnList<Row, Columns>): InsertBuilder<Row, Selected<Row, Columns>[]>;
|
|
186
|
+
protected build(): Query;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* `naro.from(table).update(…)` and `.delete()` — resolve to null, or the rows
|
|
190
|
+
* with `.select()`. Without a filter the worker refuses them (FILTER_REQUIRED)
|
|
191
|
+
* unless `.all()` says so on purpose, and only the service key may.
|
|
192
|
+
*/
|
|
193
|
+
export declare class MutationBuilder<Row, Result = null> extends FilterBuilder<Row, Result> {
|
|
194
|
+
/** Return the changed rows (all columns, or the ones named). */
|
|
195
|
+
select<Columns extends string = "*">(columns?: ColumnList<Row, Columns>): MutationBuilder<Row, Selected<Row, Columns>[]>;
|
|
196
|
+
/** Apply to every row with no filter at all. Service key only. */
|
|
197
|
+
all(): this;
|
|
198
|
+
protected build(): Query;
|
|
199
|
+
}
|
|
200
|
+
/** `naro.from(table)` — pick the action. */
|
|
201
|
+
export declare class TableBuilder<Row, Insert, Update> {
|
|
202
|
+
#private;
|
|
203
|
+
constructor(run: Runner, table: string);
|
|
204
|
+
/** `"*"` (the default) or `"id, title"`. */
|
|
205
|
+
select<Columns extends string = "*">(columns?: ColumnList<Row, Columns>): SelectBuilder<Row, Selected<Row, Columns>[], Selected<Row, Columns>>;
|
|
206
|
+
/** One row or several; one request, one transaction. */
|
|
207
|
+
insert(rows: Insert | readonly Insert[]): InsertBuilder<Row>;
|
|
208
|
+
update(values: Update): MutationBuilder<Row>;
|
|
209
|
+
delete(): MutationBuilder<Row>;
|
|
210
|
+
}
|
|
211
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `select("id, title")` → `["id", "title"]`; `select()` or `select("*")` →
|
|
3
|
+
* `"*"`. Names are comma-separated and trimmed. A name with a comma or edge
|
|
4
|
+
* spaces goes in double quotes, with `""` for a quote inside it:
|
|
5
|
+
* `select('"a, b", c')` → `["a, b", "c"]`. Whether a column exists is the
|
|
6
|
+
* worker's call; this only refuses what can never be a name.
|
|
7
|
+
*/
|
|
8
|
+
export type ParsedColumns = {
|
|
9
|
+
ok: true;
|
|
10
|
+
columns: string[] | "*";
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
reason: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function parseColumns(input: string | undefined): ParsedColumns;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Filter, FilterNode, FilterOperator } from "../wire.js";
|
|
2
|
+
/**
|
|
3
|
+
* data-core's MAX_FILTER_DEPTH; `wire.test.ts` checks that the two agree.
|
|
4
|
+
* Checked here only so a string of 10,000 `and(` cannot exhaust the stack;
|
|
5
|
+
* the worker counts the whole tree.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MAX_FILTER_DEPTH = 4;
|
|
8
|
+
/** The operator names `.filter()` and `.not()` take: the wire's and PostgREST's `cs`, `cd`, `ov`. */
|
|
9
|
+
export type FilterOperatorName = FilterOperator | "cs" | "cd" | "ov";
|
|
10
|
+
export type Parsed<T> = {
|
|
11
|
+
ok: true;
|
|
12
|
+
node: T;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
reason: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* `tokens`: words the caller gives a value of its own — where a value or a
|
|
19
|
+
* list element goes, the word unquoted and alone stands for that value. The
|
|
20
|
+
* CLI reads a policy's `auth.uid()` this way (P2-2); the SDK passes none.
|
|
21
|
+
* The same word unquoted without its `()` or in another case is refused
|
|
22
|
+
* rather than read as text (사용자 리뷰 반영): `user_id.neq.auth.uid` would
|
|
23
|
+
* otherwise hold for every row.
|
|
24
|
+
*
|
|
25
|
+
* `group`: whether the items sit in a group of the caller's, `.or()`'s or
|
|
26
|
+
* `.and()`'s, which counts as the first of the four levels (the default).
|
|
27
|
+
* The CLI's condition has none when it is one item (사용자 리뷰 반영): it
|
|
28
|
+
* sends that item alone, so four groups inside it are four, not five.
|
|
29
|
+
*/
|
|
30
|
+
export type ParseOptions = {
|
|
31
|
+
tokens?: Readonly<Record<string, unknown>>;
|
|
32
|
+
group?: boolean;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* PostgREST's `*` for `%` in a `like` / `ilike` pattern, everywhere a pattern
|
|
36
|
+
* is taken (사용자 리뷰 반영: `.like()` and `.ilike()` too, so ported
|
|
37
|
+
* supabase-js code matches). As in PostgREST, a literal `*` cannot be matched.
|
|
38
|
+
*/
|
|
39
|
+
export declare function likePattern(pattern: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* `value` as one quoted filter-string value: `\` and `"` escaped, in that
|
|
42
|
+
* order, and the whole in double quotes. Whatever the text — `,` `.` `(` `)`,
|
|
43
|
+
* quotes, backslashes — the parser reads it back as exactly this one value,
|
|
44
|
+
* so text from user input cannot close the quote early and add a condition
|
|
45
|
+
* (적대적 리뷰 반영). Quoted, a value stays text everywhere, a number in a
|
|
46
|
+
* `cs.{…}` list included. It does not escape a `like` pattern's `%`, `_` or
|
|
47
|
+
* `*`, which keep their meaning.
|
|
48
|
+
*/
|
|
49
|
+
export declare function quoteFilterValue(value: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* `.or(text)` / `.and(text)`: the items, in the method's own group. The group
|
|
52
|
+
* counts as the first of the four levels. With `group: false` (the CLI), one
|
|
53
|
+
* item comes back alone and counts from zero; several still come in `kind`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseLogic(text: unknown, kind: "or" | "and", options?: ParseOptions): Parsed<FilterNode>;
|
|
56
|
+
/**
|
|
57
|
+
* `.filter(column, operator, value)` and `.not(column, operator, value)`. A
|
|
58
|
+
* string value is PostgREST syntax where the operator needs one — a list for
|
|
59
|
+
* `in` and the array operators, a keyword for `is`, `*` for `like`, and
|
|
60
|
+
* `true` / `false` for `eq` and `neq` — and plain text otherwise. Anything
|
|
61
|
+
* else is sent as it is, for the worker to check.
|
|
62
|
+
*/
|
|
63
|
+
export declare function conditionOf(column: string, name: unknown, value: unknown): Parsed<Filter>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ColumnName } from "../types.js";
|
|
2
|
+
/** What `columns.ts` trims around a name, as far as the types can see. */
|
|
3
|
+
type Space = " " | "\t" | "\n" | "\r";
|
|
4
|
+
type Trim<Text extends string> = Text extends `${Space}${infer Rest}` ? Trim<Rest> : Text extends `${infer Rest}${Space}` ? Trim<Rest> : Text;
|
|
5
|
+
/** Tail-recursive with an accumulator, so a long list stays within TS's limit. */
|
|
6
|
+
type Split<Text extends string, Names extends string = never> = Text extends `${infer Name},${infer Rest}` ? Split<Rest, Names | Trim<Name>> : Names | Trim<Text>;
|
|
7
|
+
/**
|
|
8
|
+
* The names a column list asks for; `"*"` for every column; `null` when the
|
|
9
|
+
* types cannot read it — a string that is not a literal, or a quoted name
|
|
10
|
+
* (`'"a, b", c'`), which only the runtime parser unquotes.
|
|
11
|
+
*/
|
|
12
|
+
type Requested<Columns extends string> = string extends Columns ? null : Columns extends `${string}"${string}` ? null : Trim<Columns> extends "*" ? "*" : Split<Columns>;
|
|
13
|
+
/** An untyped row (`Record<string, unknown>`) has every column. */
|
|
14
|
+
type IsUntyped<Row> = string extends ColumnName<Row> ? true : false;
|
|
15
|
+
/** The names in the list that are not columns of the row. */
|
|
16
|
+
type Unknown<Row, Columns extends string> = IsUntyped<Row> extends true ? never : Requested<Columns> extends infer Names ? [Names] extends [null] ? never : [Names] extends ["*"] ? never : Exclude<Names, ColumnName<Row>> : never;
|
|
17
|
+
/**
|
|
18
|
+
* The type of `select()`'s argument: the list itself when every name is a
|
|
19
|
+
* column, and otherwise a string it cannot match, which names the culprit —
|
|
20
|
+
* `Argument of type '"id, nope"' is not assignable to parameter of type
|
|
21
|
+
* '"Unknown column 'nope'"'`. TypeScript infers `Columns` from the argument
|
|
22
|
+
* through both branches, so the list is checked where it is written. An
|
|
23
|
+
* empty name (`"id,"`) and `*` among names are not columns either: the
|
|
24
|
+
* runtime refuses both unsent.
|
|
25
|
+
*/
|
|
26
|
+
export type ColumnList<Row, Columns extends string> = [
|
|
27
|
+
Unknown<Row, Columns>
|
|
28
|
+
] extends [never] ? Columns : `Unknown column '${Extract<Unknown<Row, Columns>, string>}'`;
|
|
29
|
+
/**
|
|
30
|
+
* The row a select or a returning resolves to: the whole row for `"*"`,
|
|
31
|
+
* nothing given, a list the types cannot read or an untyped row, and the
|
|
32
|
+
* picked columns otherwise. Never `never`.
|
|
33
|
+
*/
|
|
34
|
+
export type Selected<Row, Columns extends string> = IsUntyped<Row> extends true ? Row : Requested<Columns> extends infer Names ? [Names] extends [null] ? Row : [Names] extends ["*"] ? Row : Pick<Row, Extract<Names, keyof Row>> : Row;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A page with a DOM: `window` and `document` both. Decides only where the
|
|
3
|
+
* token is kept by default (localStorage there, memory elsewhere); whether a
|
|
4
|
+
* service key is allowed is `isKnownServer`'s question, which is stricter.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isBrowser(scope?: object): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Whether a service key may be used here (spec §7): only where the runtime
|
|
9
|
+
* says it is a server and nothing says it is a user's device. Anything else —
|
|
10
|
+
* a browser tab, a web worker, React Native, a runtime the SDK does not
|
|
11
|
+
* recognise — is refused, since a key that reached any of them has shipped.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isKnownServer(scope?: object): boolean;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { NaroError } from "./errors.js";
|
|
2
|
+
import type { Value } from "./wire.js";
|
|
3
|
+
/**
|
|
4
|
+
* What every query resolves to (spec §15). The two halves never mix, so
|
|
5
|
+
* checking `error` narrows `data`.
|
|
6
|
+
*/
|
|
7
|
+
export type NaroResponse<T> = {
|
|
8
|
+
data: T;
|
|
9
|
+
error: null;
|
|
10
|
+
} | {
|
|
11
|
+
data: null;
|
|
12
|
+
error: NaroError;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A table's types. `naro gen types` writes all three; by hand, `Insert` and
|
|
16
|
+
* `Update` may be left out and default to `Partial<Row>`.
|
|
17
|
+
*/
|
|
18
|
+
export type GenericTable = {
|
|
19
|
+
Row: object;
|
|
20
|
+
Insert?: object;
|
|
21
|
+
Update?: object;
|
|
22
|
+
};
|
|
23
|
+
/** A view's types: it is read, never written. */
|
|
24
|
+
export type GenericView = {
|
|
25
|
+
Row: object;
|
|
26
|
+
};
|
|
27
|
+
/** The default: any table, any column, rows of unknown values. */
|
|
28
|
+
export type UntypedDatabase = {
|
|
29
|
+
tables: Record<string, {
|
|
30
|
+
Row: Record<string, unknown>;
|
|
31
|
+
Insert: Record<string, unknown>;
|
|
32
|
+
Update: Record<string, unknown>;
|
|
33
|
+
}>;
|
|
34
|
+
views: Record<never, never>;
|
|
35
|
+
};
|
|
36
|
+
/** True for `any` alone: `createClient<any>()` reads as the untyped client. */
|
|
37
|
+
type IsAny<T> = 0 extends 1 & T ? true : false;
|
|
38
|
+
/** `Database["tables"]`, where there is one. */
|
|
39
|
+
export type TablesOf<Database> = IsAny<Database> extends true ? UntypedDatabase["tables"] : Database extends {
|
|
40
|
+
tables: infer Tables;
|
|
41
|
+
} ? Tables : Record<never, never>;
|
|
42
|
+
/** `Database["views"]`, or none. */
|
|
43
|
+
export type ViewsOf<Database> = Database extends {
|
|
44
|
+
views: infer Views;
|
|
45
|
+
} ? Views : Record<never, never>;
|
|
46
|
+
/**
|
|
47
|
+
* `createClient<Database>()` takes what `naro gen types` writes (spec
|
|
48
|
+
* 2026-09-24 P2-3 §2): `{ tables: { [name]: { Row; Insert; Update } };
|
|
49
|
+
* views: { [name]: { Row } } }`. Mapped types, not index signatures, so an
|
|
50
|
+
* `interface` fits too; `views` may be left out by hand. Phase 1's flat
|
|
51
|
+
* `{ [table]: { Row } }` has no `tables` and is refused, not read as `any`.
|
|
52
|
+
*/
|
|
53
|
+
export type DatabaseShape<Database> = {
|
|
54
|
+
tables: {
|
|
55
|
+
[Table in keyof TablesOf<Database>]: GenericTable;
|
|
56
|
+
};
|
|
57
|
+
views?: {
|
|
58
|
+
[View in keyof ViewsOf<Database>]: GenericView;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The names `from()` takes: the tables. Not the views — the Data API reads
|
|
63
|
+
* no view, and answers one with 404 TABLE_NOT_FOUND (spec P2-3 §5.2).
|
|
64
|
+
*/
|
|
65
|
+
export type TableName<Database> = Extract<keyof TablesOf<Database>, string>;
|
|
66
|
+
export type ColumnName<Row> = Extract<keyof Row, string>;
|
|
67
|
+
export type RowOf<Table> = Table extends {
|
|
68
|
+
Row: infer Row extends object;
|
|
69
|
+
} ? Row : never;
|
|
70
|
+
export type InsertOf<Table> = Table extends {
|
|
71
|
+
Insert: infer Insert extends object;
|
|
72
|
+
} ? Insert : Partial<RowOf<Table>>;
|
|
73
|
+
export type UpdateOf<Table> = Table extends {
|
|
74
|
+
Update: infer Update extends object;
|
|
75
|
+
} ? Update : Partial<RowOf<Table>>;
|
|
76
|
+
/** A value `eq`, `neq`, `gt`… compare with. */
|
|
77
|
+
export type FilterValue = Value;
|
|
78
|
+
export type OrderOptions = {
|
|
79
|
+
/** Default true. */
|
|
80
|
+
ascending?: boolean;
|
|
81
|
+
/** Where NULLs go. Omitted, SQLite's default: first ascending, last descending. */
|
|
82
|
+
nullsFirst?: boolean;
|
|
83
|
+
};
|
|
84
|
+
/** Better Auth's user (spec §13), as JSON: dates are ISO strings. */
|
|
85
|
+
export type User = {
|
|
86
|
+
id: string;
|
|
87
|
+
email: string;
|
|
88
|
+
name: string;
|
|
89
|
+
emailVerified: boolean;
|
|
90
|
+
image?: string | null;
|
|
91
|
+
createdAt: string;
|
|
92
|
+
updatedAt: string;
|
|
93
|
+
[field: string]: unknown;
|
|
94
|
+
};
|
|
95
|
+
/** Better Auth's session, as JSON. */
|
|
96
|
+
export type Session = {
|
|
97
|
+
id: string;
|
|
98
|
+
userId: string;
|
|
99
|
+
expiresAt: string;
|
|
100
|
+
createdAt: string;
|
|
101
|
+
updatedAt: string;
|
|
102
|
+
[field: string]: unknown;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Auth results keep `data` an object even on failure, so
|
|
106
|
+
* `const { data: { user } } = await naro.auth.getUser()` never destructures
|
|
107
|
+
* null (spec §1, §15).
|
|
108
|
+
*/
|
|
109
|
+
export type AuthResponse<T> = {
|
|
110
|
+
data: T;
|
|
111
|
+
error: NaroError | null;
|
|
112
|
+
};
|
|
113
|
+
export type UserResponse = AuthResponse<{
|
|
114
|
+
user: User | null;
|
|
115
|
+
}>;
|
|
116
|
+
export type SessionResponse = AuthResponse<{
|
|
117
|
+
session: Session | null;
|
|
118
|
+
user: User | null;
|
|
119
|
+
}>;
|
|
120
|
+
export type SignOutResponse = {
|
|
121
|
+
data: null;
|
|
122
|
+
error: NaroError | null;
|
|
123
|
+
};
|
|
124
|
+
export type SignUpCredentials = {
|
|
125
|
+
email: string;
|
|
126
|
+
password: string;
|
|
127
|
+
/** Better Auth requires a name; omitted, the email's local part is sent. */
|
|
128
|
+
name?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Where the confirmation link sends the browser back to, when the project
|
|
131
|
+
* sends one (P2-6): a URL on an allowed origin. Omitted, the project's own
|
|
132
|
+
* origin.
|
|
133
|
+
*/
|
|
134
|
+
redirectTo?: string;
|
|
135
|
+
};
|
|
136
|
+
export type SignInCredentials = {
|
|
137
|
+
email: string;
|
|
138
|
+
password: string;
|
|
139
|
+
};
|
|
140
|
+
export type OAuthProvider = "google" | "github";
|
|
141
|
+
export type SignInWithOAuthOptions = {
|
|
142
|
+
provider: OAuthProvider;
|
|
143
|
+
/**
|
|
144
|
+
* Where the provider's callback sends the browser back to: an allowed
|
|
145
|
+
* origin of the project (`naro backend auth redirects add`). Omitted in a
|
|
146
|
+
* browser, the current page.
|
|
147
|
+
*/
|
|
148
|
+
redirectTo?: string;
|
|
149
|
+
/** Answer the sign-in URL without navigating to it. */
|
|
150
|
+
skipBrowserRedirect?: boolean;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* `url` is where the browser goes to sign in — the worker's
|
|
154
|
+
* `/_naro/v1/auth/authorize/<provider>`, which goes on to the provider; null
|
|
155
|
+
* when it could not start.
|
|
156
|
+
*/
|
|
157
|
+
export type OAuthResponse = AuthResponse<{
|
|
158
|
+
provider: OAuthProvider;
|
|
159
|
+
url: string | null;
|
|
160
|
+
}>;
|
|
161
|
+
/** What brought the browser back: a provider, a confirmation link, a reset link. */
|
|
162
|
+
export type AuthFlowType = "oauth" | "email_verification" | "recovery";
|
|
163
|
+
/**
|
|
164
|
+
* `getSessionFromUrl()`: `type` null when the URL carried nothing of Naro's.
|
|
165
|
+
* `recovery` holds no session — the page asks for a new password and calls
|
|
166
|
+
* `updateUser({ password })`.
|
|
167
|
+
*/
|
|
168
|
+
export type UrlSessionResponse = AuthResponse<{
|
|
169
|
+
session: Session | null;
|
|
170
|
+
user: User | null;
|
|
171
|
+
type: AuthFlowType | null;
|
|
172
|
+
}>;
|
|
173
|
+
export type EmailRedirectOptions = {
|
|
174
|
+
/** A URL on an allowed origin of the project; omitted, its own origin. */
|
|
175
|
+
redirectTo?: string;
|
|
176
|
+
};
|
|
177
|
+
/** What `updateUser` changes: the password, in the recovery flow. */
|
|
178
|
+
export type UserAttributes = {
|
|
179
|
+
password: string;
|
|
180
|
+
};
|
|
181
|
+
/** A call that answers nothing but whether it worked. */
|
|
182
|
+
export type AuthActionResponse = {
|
|
183
|
+
data: null;
|
|
184
|
+
error: NaroError | null;
|
|
185
|
+
};
|
|
186
|
+
export {};
|
package/dist/wire.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type Value = string | number | boolean | null;
|
|
2
|
+
/** Objects and arrays are stored as their JSON text. */
|
|
3
|
+
export type Row = Record<string, Value | object>;
|
|
4
|
+
export type FilterOperator = "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" | "is" | "like" | "ilike" | "contains" | "containedBy" | "overlaps";
|
|
5
|
+
/** One condition: a leaf of a filter tree. */
|
|
6
|
+
export type Filter = {
|
|
7
|
+
operator: FilterOperator;
|
|
8
|
+
column: string;
|
|
9
|
+
value: Value | Value[];
|
|
10
|
+
};
|
|
11
|
+
/** A condition or a group of them (Phase 2-1). `filters` itself is an AND. */
|
|
12
|
+
export type FilterNode = Filter | {
|
|
13
|
+
or: FilterNode[];
|
|
14
|
+
} | {
|
|
15
|
+
and: FilterNode[];
|
|
16
|
+
} | {
|
|
17
|
+
not: FilterNode;
|
|
18
|
+
};
|
|
19
|
+
export type Order = {
|
|
20
|
+
column: string;
|
|
21
|
+
ascending: boolean;
|
|
22
|
+
nullsFirst?: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type Returning = string[] | "*";
|
|
25
|
+
export type SelectQuery = {
|
|
26
|
+
version: 1;
|
|
27
|
+
action: "select";
|
|
28
|
+
table: string;
|
|
29
|
+
columns: string[] | "*";
|
|
30
|
+
filters?: FilterNode[];
|
|
31
|
+
order?: Order[];
|
|
32
|
+
limit?: number;
|
|
33
|
+
offset?: number;
|
|
34
|
+
single?: "one" | "maybe";
|
|
35
|
+
};
|
|
36
|
+
export type InsertQuery = {
|
|
37
|
+
version: 1;
|
|
38
|
+
action: "insert";
|
|
39
|
+
table: string;
|
|
40
|
+
rows: Row[];
|
|
41
|
+
returning?: Returning;
|
|
42
|
+
};
|
|
43
|
+
export type UpdateQuery = {
|
|
44
|
+
version: 1;
|
|
45
|
+
action: "update";
|
|
46
|
+
table: string;
|
|
47
|
+
values: Row;
|
|
48
|
+
filters?: FilterNode[];
|
|
49
|
+
returning?: Returning;
|
|
50
|
+
allowUnfiltered?: true;
|
|
51
|
+
};
|
|
52
|
+
export type DeleteQuery = {
|
|
53
|
+
version: 1;
|
|
54
|
+
action: "delete";
|
|
55
|
+
table: string;
|
|
56
|
+
filters?: FilterNode[];
|
|
57
|
+
returning?: Returning;
|
|
58
|
+
allowUnfiltered?: true;
|
|
59
|
+
};
|
|
60
|
+
export type Query = SelectQuery | InsertQuery | UpdateQuery | DeleteQuery;
|
|
61
|
+
export type QueryAction = Query["action"];
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "naro-js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Auth and data for your Naro project, from the browser or the server.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "24.13.3",
|
|
22
|
+
"miniflare": "5.20260815.0-alpha",
|
|
23
|
+
"tsup": "8.5.0",
|
|
24
|
+
"typescript": "7.0.2",
|
|
25
|
+
"vitest": "3.2.7",
|
|
26
|
+
"@next-deploy/backend-worker": "0.1.0",
|
|
27
|
+
"@next-deploy/config": "0.1.0",
|
|
28
|
+
"@next-deploy/sdk": "0.1.0",
|
|
29
|
+
"@next-deploy/data-core": "0.1.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup && pnpm build:types",
|
|
33
|
+
"build:types": "tsc --project tsconfig.build.json",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"typecheck": "tsc --noEmit && tsc --project tsconfig.build.json --noEmit"
|
|
36
|
+
}
|
|
37
|
+
}
|