@rayfold/postgres 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/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +67 -0
- package/index.d.ts +124 -0
- package/index.js +435 -0
- package/index.js.map +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Rayfold
|
|
2
|
+
Copyright 2026 The Rayfold Authors
|
|
3
|
+
|
|
4
|
+
This product is licensed under the Apache License, Version 2.0 (see LICENSE).
|
|
5
|
+
|
|
6
|
+
The data in data/gutenberg.json.gz is derived from the Project Gutenberg catalogue
|
|
7
|
+
(https://www.gutenberg.org/ebooks/offline_catalogs.html) and is not covered by this
|
|
8
|
+
project's copyright. See data/README.md for its source. Project Gutenberg is a
|
|
9
|
+
registered trademark of the Project Gutenberg Literary Archive Foundation; Rayfold is
|
|
10
|
+
not affiliated with or endorsed by Project Gutenberg.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @rayfold/postgres
|
|
2
|
+
|
|
3
|
+
Postgres for [Rayfold](https://github.com/eddyboutros/rayfold) resolvers: batch loads by id, keyset pages, one query
|
|
4
|
+
per level of a nested shape, and read policies pushed into the SQL `WHERE`, so a list never fetches rows its viewer
|
|
5
|
+
may not see.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @rayfold/postgres pg
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import pg from "pg";
|
|
13
|
+
import { createRayfoldServer } from "@rayfold/server";
|
|
14
|
+
import { createPgStore } from "@rayfold/postgres";
|
|
15
|
+
|
|
16
|
+
const db = new pg.Pool({ connectionString: process.env.DATABASE_URL });
|
|
17
|
+
const schema = `
|
|
18
|
+
entity Author { id: ID name: String books(page: PageArgs = { first: 10 }): Page<Book> }
|
|
19
|
+
entity Book { id: ID title: String author: Author }
|
|
20
|
+
entity Order @allow(read: viewer.id == customerId) { id: ID customerId: ID total: Decimal }
|
|
21
|
+
query books(page: PageArgs = { first: 20 }): Page<Book>
|
|
22
|
+
query myOrders: [Order]
|
|
23
|
+
`;
|
|
24
|
+
const server = createRayfoldServer({ schema, resolvers: {} }); // for its IR; see below
|
|
25
|
+
const store = createPgStore(db, {
|
|
26
|
+
ir: server.ir,
|
|
27
|
+
naming: "snake", // authorId -> author_id
|
|
28
|
+
tables: { Author: { table: "authors" }, Book: { table: "books", columns: { authorId: "author_id" } }, Order: { table: "orders" } },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const resolvers = {
|
|
32
|
+
Query: {
|
|
33
|
+
books: (args, ctx) => store.page("Book", args.page, {}, ctx),
|
|
34
|
+
myOrders: (_args, ctx) => store.find("Order", {}, ctx), // the policy becomes WHERE customer_id = $viewer
|
|
35
|
+
},
|
|
36
|
+
Book: { author: (books, _args, ctx) => store.byIds("Author", books.map((b) => b.authorId), ctx) },
|
|
37
|
+
Author: { books: (authors, args, ctx) => store.pagesByField("Book", "authorId", authors.map((a) => a.id), args.page, ctx) },
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Any client with `query(text, params)` works: `pg.Pool`, `pg.Client`, [PGlite](https://pglite.dev), or your own
|
|
42
|
+
wrapper (for logging or transactions).
|
|
43
|
+
|
|
44
|
+
## What each call does
|
|
45
|
+
|
|
46
|
+
| Call | SQL | Use it for |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `byIds(type, ids, ctx)` | one `SELECT ... WHERE id = ANY($1)` | a to-one field's batch loader; returns rows in the order asked, `null` for a missing or hidden row |
|
|
49
|
+
| `find(type, where, ctx)` | one `SELECT ... WHERE field = $n ... ORDER BY id` | a short list |
|
|
50
|
+
| `page(type, { first, after }, where, ctx)` | one query with `count(*) OVER ()` | a `Page<T>` in key order: `items`, `cursor`, `hasMore`, `total` |
|
|
51
|
+
| `pagesByField(type, field, parents, { first, after }, ctx)` | one query with window functions | a paged one-to-many field for a whole level of parents at once |
|
|
52
|
+
|
|
53
|
+
## Read policies in SQL
|
|
54
|
+
|
|
55
|
+
When a resolver loads an entity whose read policy is pushable (it reads only the viewer, the arguments, literals and
|
|
56
|
+
the row's own scalar fields; spec 06 §4), the runtime hands it to the resolver as `ctx.policy.filter`. Pass `ctx` to the
|
|
57
|
+
store and the policy becomes part of the `WHERE` clause: a customer's list query reads only their rows, and a page's
|
|
58
|
+
`total` counts only what they may see.
|
|
59
|
+
|
|
60
|
+
The translation never drops a row the policy allows. Where SQL cannot match the runtime exactly (ordering text,
|
|
61
|
+
comparing values of different types, `Decimal` inequality) it lets the row through and the runtime's own check, which
|
|
62
|
+
always runs, removes it. Without the pushdown, a list that returns rows the viewer may not see is refused as a whole
|
|
63
|
+
(spec 06 §3); with it, the list holds exactly the permitted rows.
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
Apache-2.0
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres for Rayfold resolvers: batch loads by id, keyset pages, one-to-many pages for a whole level in one query,
|
|
3
|
+
* and read policies pushed into the SQL `WHERE` (spec 06 §4), so a list never fetches rows its viewer may not see.
|
|
4
|
+
*
|
|
5
|
+
* Works with any client that has `query(text, params)`: pg's `Pool` and `Client`, PGlite, or a wrapper of your own.
|
|
6
|
+
* Every value travels as a bound parameter and every identifier is quoted.
|
|
7
|
+
*
|
|
8
|
+
* The pushed-down filter is a superset of the policy: it may let through a row the policy denies (the runtime's own
|
|
9
|
+
* check then removes it), but it never drops a row the policy allows. Comparisons it cannot translate exactly, such as
|
|
10
|
+
* ordering text or comparing across types, are left to the runtime.
|
|
11
|
+
*/
|
|
12
|
+
import { type Expr, type ExprEnv, type RayfoldSchemaIR, type Shape } from "@rayfold/schema";
|
|
13
|
+
/** Anything with pg's `query(text, params)`: `pg.Pool`, `pg.Client`, `PGlite`. */
|
|
14
|
+
export interface Queryable {
|
|
15
|
+
query<R = Record<string, unknown>>(text: string, params?: unknown[]): Promise<{
|
|
16
|
+
rows: R[];
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export interface TableMapping {
|
|
20
|
+
/** The table, optionally schema-qualified (`"public.books"`). */
|
|
21
|
+
table: string;
|
|
22
|
+
/** The key column; default `"id"`. Pages are ordered by it. */
|
|
23
|
+
id?: string;
|
|
24
|
+
/** Field name to column name, for fields whose column is named otherwise (after `naming`). */
|
|
25
|
+
columns?: Record<string, string>;
|
|
26
|
+
/** Fields that lead to another mapped type. `screen` needs these; the batch loaders do not. */
|
|
27
|
+
relations?: Record<string, Relation>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* How a field reaches another table. `one`: `key` is the field on *this* type holding the other row's key
|
|
31
|
+
* (`Book.authorId`). `page`: `key` is the field on the *other* type holding this row's key (`Book.authorId`, as seen
|
|
32
|
+
* from `Author.books`).
|
|
33
|
+
*/
|
|
34
|
+
export interface Relation {
|
|
35
|
+
type: string;
|
|
36
|
+
kind: "one" | "page";
|
|
37
|
+
key: string;
|
|
38
|
+
}
|
|
39
|
+
export interface PgStoreOptions {
|
|
40
|
+
ir: RayfoldSchemaIR;
|
|
41
|
+
/** One mapping per entity type the store serves. */
|
|
42
|
+
tables: Record<string, TableMapping>;
|
|
43
|
+
/** How field names become column names when `columns` does not say: as they are (default) or snake_case. */
|
|
44
|
+
naming?: "same" | "snake";
|
|
45
|
+
}
|
|
46
|
+
/** What the store needs from a resolver's context: the viewer and the read policy the runtime pushed down (`ctx.policy`). */
|
|
47
|
+
export interface PolicyContext {
|
|
48
|
+
viewer?: unknown;
|
|
49
|
+
policy?: {
|
|
50
|
+
filter?: Expr;
|
|
51
|
+
};
|
|
52
|
+
now?: () => number;
|
|
53
|
+
}
|
|
54
|
+
export type Row = Record<string, unknown>;
|
|
55
|
+
export interface Page<T = Row> {
|
|
56
|
+
items: T[];
|
|
57
|
+
cursor: string | null;
|
|
58
|
+
hasMore: boolean;
|
|
59
|
+
total: number;
|
|
60
|
+
}
|
|
61
|
+
export interface PageRequest {
|
|
62
|
+
first: number;
|
|
63
|
+
after?: string | null;
|
|
64
|
+
}
|
|
65
|
+
/** A SQL fragment with its parameters numbered from 1. */
|
|
66
|
+
export interface Fragment {
|
|
67
|
+
sql: string;
|
|
68
|
+
exact: boolean;
|
|
69
|
+
}
|
|
70
|
+
export declare function createPgStore(db: Queryable, opts: PgStoreOptions): PgStore;
|
|
71
|
+
export declare class PgStore {
|
|
72
|
+
private readonly db;
|
|
73
|
+
private readonly opts;
|
|
74
|
+
private readonly tables;
|
|
75
|
+
constructor(db: Queryable, opts: PgStoreOptions);
|
|
76
|
+
/**
|
|
77
|
+
* Rows of `type` for `ids`, in the order asked: `null` where a row is missing or hidden by the pushed-down read policy.
|
|
78
|
+
* One query for any number of ids, which makes it the batch loader for a to-one field.
|
|
79
|
+
*/
|
|
80
|
+
byIds(type: string, ids: readonly unknown[], ctx?: PolicyContext, args?: Record<string, unknown>): Promise<Array<Row | null>>;
|
|
81
|
+
/** Rows of `type` whose fields equal `where` (a `null` value matches NULL), in key order. One query. */
|
|
82
|
+
find(type: string, where?: Record<string, unknown>, ctx?: PolicyContext, args?: Record<string, unknown>): Promise<Row[]>;
|
|
83
|
+
/**
|
|
84
|
+
* One page of `type` in key order, after `page.after` (a key), with `total` counting every visible matching row.
|
|
85
|
+
* One query; a second one counts only when the page comes back empty.
|
|
86
|
+
*/
|
|
87
|
+
page(type: string, page: PageRequest, where?: Record<string, unknown>, ctx?: PolicyContext, args?: Record<string, unknown>): Promise<Page>;
|
|
88
|
+
/**
|
|
89
|
+
* For each parent key in `values`, the page of `type` rows whose `field` equals it: the batch loader for a paged
|
|
90
|
+
* one-to-many field (an author's books). One query for the whole level, whatever the number of parents.
|
|
91
|
+
*/
|
|
92
|
+
pagesByField(type: string, field: string, values: readonly unknown[], page: PageRequest, ctx?: PolicyContext, args?: Record<string, unknown>): Promise<Page[]>;
|
|
93
|
+
/**
|
|
94
|
+
* One statement for a whole screen: a page of `type` with every nested field the shape asks for, gathered as JSON
|
|
95
|
+
* by correlated subqueries. Depth costs no extra round trip, and each level's read policy is pushed into its own
|
|
96
|
+
* `WHERE`, so a nested list never carries rows its viewer may not see.
|
|
97
|
+
*
|
|
98
|
+
* Nested pages are first pages (`first`), which is what a screen shows; the root page takes a cursor as usual.
|
|
99
|
+
* Every selected field must be a mapped column or a declared relation.
|
|
100
|
+
*/
|
|
101
|
+
screen(type: string, shape: Shape, page: PageRequest, where?: Record<string, unknown>, ctx?: PolicyContext, args?: Record<string, unknown>): Promise<Page>;
|
|
102
|
+
/** The JSON projection of one level of a screen: scalars from columns, relations from correlated subqueries. */
|
|
103
|
+
private project;
|
|
104
|
+
/** The WHERE fragment for the read policy the runtime pushed down, or nothing when there is none to push. */
|
|
105
|
+
private policyWhere;
|
|
106
|
+
/** The read policy declared on a type, for the levels of a screen the runtime handed no hint for. */
|
|
107
|
+
private filterFor;
|
|
108
|
+
private equalities;
|
|
109
|
+
private select;
|
|
110
|
+
private row;
|
|
111
|
+
private table;
|
|
112
|
+
private scalarOf;
|
|
113
|
+
}
|
|
114
|
+
/** A column a policy may read, with the Rayfold scalar type of its field. */
|
|
115
|
+
export interface PolicyColumn {
|
|
116
|
+
column: string;
|
|
117
|
+
scalar: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Translates a pushable read policy (spec 06 §4) into a WHERE fragment under `env` (the viewer and args), appending
|
|
121
|
+
* its values to `params`. Parts that read no field of the row are evaluated here, as the runtime would. `exact` is true
|
|
122
|
+
* when the fragment selects exactly the rows the policy allows; otherwise it selects more, never fewer.
|
|
123
|
+
*/
|
|
124
|
+
export declare function compilePolicy(e: Expr, env: ExprEnv, columnOf: (field: string) => PolicyColumn | undefined, params: unknown[]): Fragment;
|
package/index.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres for Rayfold resolvers: batch loads by id, keyset pages, one-to-many pages for a whole level in one query,
|
|
3
|
+
* and read policies pushed into the SQL `WHERE` (spec 06 §4), so a list never fetches rows its viewer may not see.
|
|
4
|
+
*
|
|
5
|
+
* Works with any client that has `query(text, params)`: pg's `Pool` and `Client`, PGlite, or a wrapper of your own.
|
|
6
|
+
* Every value travels as a bound parameter and every identifier is quoted.
|
|
7
|
+
*
|
|
8
|
+
* The pushed-down filter is a superset of the policy: it may let through a row the policy denies (the runtime's own
|
|
9
|
+
* check then removes it), but it never drops a row the policy allows. Comparisons it cannot translate exactly, such as
|
|
10
|
+
* ordering text or comparing across types, are left to the runtime.
|
|
11
|
+
*/
|
|
12
|
+
import { evalExpr } from "@rayfold/schema";
|
|
13
|
+
export function createPgStore(db, opts) {
|
|
14
|
+
return new PgStore(db, opts);
|
|
15
|
+
}
|
|
16
|
+
export class PgStore {
|
|
17
|
+
db;
|
|
18
|
+
opts;
|
|
19
|
+
tables = new Map();
|
|
20
|
+
constructor(db, opts) {
|
|
21
|
+
this.db = db;
|
|
22
|
+
this.opts = opts;
|
|
23
|
+
for (const [type, m] of Object.entries(opts.tables)) {
|
|
24
|
+
const def = opts.ir.types[type];
|
|
25
|
+
if (!def || !("fields" in def))
|
|
26
|
+
throw new Error(`@rayfold/postgres: ${type} is not an entity or object of the schema`);
|
|
27
|
+
const columns = new Map();
|
|
28
|
+
for (const f of def.fields)
|
|
29
|
+
columns.set(f.name, m.columns?.[f.name] ?? (opts.naming === "snake" ? snake(f.name) : f.name));
|
|
30
|
+
for (const [f, c] of Object.entries(m.columns ?? {}))
|
|
31
|
+
columns.set(f, c);
|
|
32
|
+
const id = m.id ?? "id";
|
|
33
|
+
const idField = [...columns].find(([, c]) => c === id)?.[0] ?? "id";
|
|
34
|
+
const fields = new Map([...columns].map(([f, c]) => [c, f]));
|
|
35
|
+
this.tables.set(type, {
|
|
36
|
+
name: m.table.split(".").map(quote).join("."),
|
|
37
|
+
id: quote(id),
|
|
38
|
+
idField,
|
|
39
|
+
column: (f) => (columns.has(f) ? quote(columns.get(f)) : undefined),
|
|
40
|
+
field: (c) => fields.get(c) ?? c,
|
|
41
|
+
def: { name: def.name, fields: def.fields },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Rows of `type` for `ids`, in the order asked: `null` where a row is missing or hidden by the pushed-down read policy.
|
|
47
|
+
* One query for any number of ids, which makes it the batch loader for a to-one field.
|
|
48
|
+
*/
|
|
49
|
+
async byIds(type, ids, ctx = {}, args = {}) {
|
|
50
|
+
if (!ids.length)
|
|
51
|
+
return [];
|
|
52
|
+
const t = this.table(type);
|
|
53
|
+
const params = [[...new Set(ids.filter((x) => x !== null && x !== undefined).map(String))]];
|
|
54
|
+
const where = [`${t.id}::text = ANY($1::text[])`, ...this.policyWhere(t, ctx, args, params)];
|
|
55
|
+
const rows = await this.select(t, `SELECT * FROM ${t.name} WHERE ${where.join(" AND ")}`, params);
|
|
56
|
+
const byId = new Map(rows.map((r) => [String(r[t.idField]), r]));
|
|
57
|
+
return ids.map((id) => (id === null || id === undefined ? null : (byId.get(String(id)) ?? null)));
|
|
58
|
+
}
|
|
59
|
+
/** Rows of `type` whose fields equal `where` (a `null` value matches NULL), in key order. One query. */
|
|
60
|
+
async find(type, where = {}, ctx = {}, args = {}) {
|
|
61
|
+
const t = this.table(type);
|
|
62
|
+
const params = [];
|
|
63
|
+
const conds = [...this.equalities(t, where, params), ...this.policyWhere(t, ctx, args, params)];
|
|
64
|
+
return this.select(t, `SELECT * FROM ${t.name}${conds.length ? ` WHERE ${conds.join(" AND ")}` : ""} ORDER BY ${t.id}`, params);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* One page of `type` in key order, after `page.after` (a key), with `total` counting every visible matching row.
|
|
68
|
+
* One query; a second one counts only when the page comes back empty.
|
|
69
|
+
*/
|
|
70
|
+
async page(type, page, where = {}, ctx = {}, args = {}) {
|
|
71
|
+
const t = this.table(type);
|
|
72
|
+
const params = [];
|
|
73
|
+
const conds = [...this.equalities(t, where, params), ...this.policyWhere(t, ctx, args, params)];
|
|
74
|
+
const filtered = `SELECT *, count(*) OVER () AS "__total" FROM ${t.name}${conds.length ? ` WHERE ${conds.join(" AND ")}` : ""}`;
|
|
75
|
+
let after = "";
|
|
76
|
+
if (page.after) {
|
|
77
|
+
params.push(page.after);
|
|
78
|
+
after = ` WHERE "__s".${t.id}::text > $${params.length}::text`;
|
|
79
|
+
}
|
|
80
|
+
params.push(page.first + 1);
|
|
81
|
+
const raw = (await this.db.query(`SELECT * FROM (${filtered}) AS "__s"${after} ORDER BY "__s".${t.id}::text LIMIT $${params.length}`, params)).rows;
|
|
82
|
+
let total = raw.length ? Number(raw[0]["__total"]) : 0;
|
|
83
|
+
if (!raw.length && page.after) {
|
|
84
|
+
const count = await this.db.query(`SELECT count(*)::int AS n FROM ${t.name}${conds.length ? ` WHERE ${conds.join(" AND ")}` : ""}`, params.slice(0, params.length - 2));
|
|
85
|
+
total = Number(count.rows[0]?.n ?? 0);
|
|
86
|
+
}
|
|
87
|
+
const rows = raw.map((r) => this.row(t, r));
|
|
88
|
+
const items = rows.slice(0, page.first);
|
|
89
|
+
return { items, cursor: items.length ? String(items[items.length - 1][t.idField]) : null, hasMore: rows.length > page.first, total };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* For each parent key in `values`, the page of `type` rows whose `field` equals it: the batch loader for a paged
|
|
93
|
+
* one-to-many field (an author's books). One query for the whole level, whatever the number of parents.
|
|
94
|
+
*/
|
|
95
|
+
async pagesByField(type, field, values, page, ctx = {}, args = {}) {
|
|
96
|
+
if (!values.length)
|
|
97
|
+
return [];
|
|
98
|
+
const t = this.table(type);
|
|
99
|
+
const col = t.column(field);
|
|
100
|
+
if (!col)
|
|
101
|
+
throw new Error(`@rayfold/postgres: ${type} has no field ${field}`);
|
|
102
|
+
const params = [[...new Set(values.map(String))]];
|
|
103
|
+
const conds = [`${col}::text = ANY($1::text[])`, ...this.policyWhere(t, ctx, args, params)];
|
|
104
|
+
let after = "";
|
|
105
|
+
if (page.after) {
|
|
106
|
+
params.push(page.after);
|
|
107
|
+
after = ` AND "__s".${t.id}::text > $${params.length}::text`;
|
|
108
|
+
}
|
|
109
|
+
params.push(page.first + 1);
|
|
110
|
+
const sql = `SELECT * FROM (SELECT *, count(*) OVER (PARTITION BY ${col}) AS "__total", ` +
|
|
111
|
+
`row_number() OVER (PARTITION BY ${col} ORDER BY ${t.id}::text) AS "__n" FROM ${t.name} WHERE ${conds.join(" AND ")}) AS "__s" ` +
|
|
112
|
+
`WHERE TRUE${after} ORDER BY "__s".${t.id}::text`;
|
|
113
|
+
const raw = (await this.db.query(sql, params.slice(0, -1))).rows;
|
|
114
|
+
const groups = new Map();
|
|
115
|
+
for (const r of raw) {
|
|
116
|
+
const key = String(r[col.slice(1, -1).replace(/""/g, '"')]);
|
|
117
|
+
const g = groups.get(key) ?? { rows: [], total: Number(r["__total"]) };
|
|
118
|
+
g.rows.push(this.row(t, r));
|
|
119
|
+
groups.set(key, g);
|
|
120
|
+
}
|
|
121
|
+
return values.map((v) => {
|
|
122
|
+
const g = groups.get(String(v));
|
|
123
|
+
if (!g)
|
|
124
|
+
return { items: [], cursor: null, hasMore: false, total: 0 };
|
|
125
|
+
const items = g.rows.slice(0, page.first);
|
|
126
|
+
return { items, cursor: items.length ? String(items[items.length - 1][t.idField]) : null, hasMore: g.rows.length > page.first, total: g.total };
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* One statement for a whole screen: a page of `type` with every nested field the shape asks for, gathered as JSON
|
|
131
|
+
* by correlated subqueries. Depth costs no extra round trip, and each level's read policy is pushed into its own
|
|
132
|
+
* `WHERE`, so a nested list never carries rows its viewer may not see.
|
|
133
|
+
*
|
|
134
|
+
* Nested pages are first pages (`first`), which is what a screen shows; the root page takes a cursor as usual.
|
|
135
|
+
* Every selected field must be a mapped column or a declared relation.
|
|
136
|
+
*/
|
|
137
|
+
async screen(type, shape, page, where = {}, ctx = {}, args = {}) {
|
|
138
|
+
const t = this.table(type);
|
|
139
|
+
const params = [];
|
|
140
|
+
// The caller may pass the shape of the Page the query returns, or the shape of one row.
|
|
141
|
+
const rowShape = t.def.fields.some((f) => f.name === "items") ? shape : itemsOf(shape);
|
|
142
|
+
const projection = this.project(t, "__r0", rowShape, ctx, args, params, 0);
|
|
143
|
+
const conds = [...this.equalities(t, where, params), ...this.policyWhere(t, ctx, args, params, "__r0", ctx.policy?.filter ?? this.filterFor(type))];
|
|
144
|
+
const filtered = `SELECT ${projection} AS "__row", __r0.${t.id}::text AS "__key", count(*) OVER () AS "__total" ` +
|
|
145
|
+
`FROM ${t.name} AS __r0${conds.length ? ` WHERE ${conds.join(" AND ")}` : ""}`;
|
|
146
|
+
let after = "";
|
|
147
|
+
if (page.after) {
|
|
148
|
+
params.push(page.after);
|
|
149
|
+
after = ` WHERE "__s"."__key" > $${params.length}::text`;
|
|
150
|
+
}
|
|
151
|
+
params.push(page.first + 1);
|
|
152
|
+
const raw = (await this.db.query(`SELECT * FROM (${filtered}) AS "__s"${after} ORDER BY "__s"."__key" LIMIT $${params.length}`, params)).rows;
|
|
153
|
+
let total = raw.length ? Number(raw[0]["__total"]) : 0;
|
|
154
|
+
if (!raw.length && page.after) {
|
|
155
|
+
const cp = [];
|
|
156
|
+
const cc = [...this.equalities(t, where, cp), ...this.policyWhere(t, ctx, args, cp, "__r0", ctx.policy?.filter ?? this.filterFor(type))];
|
|
157
|
+
const counted = await this.db.query(`SELECT count(*)::int AS n FROM ${t.name} AS __r0${cc.length ? ` WHERE ${cc.join(" AND ")}` : ""}`, cp);
|
|
158
|
+
total = Number(counted.rows[0]?.n ?? 0);
|
|
159
|
+
}
|
|
160
|
+
const rows = raw.slice(0, page.first);
|
|
161
|
+
return {
|
|
162
|
+
items: rows.map((r) => r["__row"]),
|
|
163
|
+
cursor: rows.length ? String(rows[rows.length - 1]["__key"]) : null,
|
|
164
|
+
hasMore: raw.length > page.first,
|
|
165
|
+
total,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** The JSON projection of one level of a screen: scalars from columns, relations from correlated subqueries. */
|
|
169
|
+
project(t, alias, shape, ctx, args, params, depth) {
|
|
170
|
+
if (depth > 8)
|
|
171
|
+
throw new Error("@rayfold/postgres: a screen nested deeper than 8 levels");
|
|
172
|
+
const relations = this.opts.tables[t.def.name]?.relations ?? {};
|
|
173
|
+
const parts = [];
|
|
174
|
+
for (const item of shape.items) {
|
|
175
|
+
if (item.kind !== "field")
|
|
176
|
+
continue; // views and type conditions are expanded before a shape reaches the store
|
|
177
|
+
const name = item.name;
|
|
178
|
+
const out = item.alias ?? name;
|
|
179
|
+
const def = t.def.fields.find((f) => f.name === name);
|
|
180
|
+
if (!def)
|
|
181
|
+
throw new Error(`@rayfold/postgres: ${t.def.name} has no field ${name}`);
|
|
182
|
+
const rel = relations[name];
|
|
183
|
+
if (!rel) {
|
|
184
|
+
const col = t.column(name);
|
|
185
|
+
if (!col)
|
|
186
|
+
throw new Error(`@rayfold/postgres: ${t.def.name}.${name} has no column and no relation`);
|
|
187
|
+
const scalar = def.type.kind === "named" ? this.scalarOf(def.type.name) : "";
|
|
188
|
+
parts.push(`'${out}', ${alias}.${col}${AS_TEXT.has(scalar) ? "::text" : ""}`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const child = this.table(rel.type);
|
|
192
|
+
const ca = `__r${depth + 1}`;
|
|
193
|
+
// A paged field is selected through its Page envelope, so the rows are described by its `items`.
|
|
194
|
+
const selected = item.shape ?? { items: [] };
|
|
195
|
+
const childProjection = this.project(child, ca, rel.kind === "page" ? itemsOf(selected) : selected, ctx, args, params, depth + 1);
|
|
196
|
+
const childPolicy = this.policyWhere(child, ctx, args, params, ca, this.filterFor(rel.type));
|
|
197
|
+
if (rel.kind === "one") {
|
|
198
|
+
const parentCol = t.column(rel.key);
|
|
199
|
+
if (!parentCol)
|
|
200
|
+
throw new Error(`@rayfold/postgres: ${t.def.name} has no field ${rel.key}`);
|
|
201
|
+
const conds = [`${ca}.${child.id} = ${alias}.${parentCol}`, ...childPolicy];
|
|
202
|
+
parts.push(`'${out}', (SELECT ${childProjection} FROM ${child.name} AS ${ca} WHERE ${conds.join(" AND ")} LIMIT 1)`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const childCol = child.column(rel.key);
|
|
206
|
+
if (!childCol)
|
|
207
|
+
throw new Error(`@rayfold/postgres: ${rel.type} has no field ${rel.key}`);
|
|
208
|
+
params.push(firstOf(item.args) ?? 10);
|
|
209
|
+
const conds = [`${ca}.${childCol} = ${alias}.${t.id}`, ...childPolicy];
|
|
210
|
+
const rows = `SELECT ${childProjection} AS "__row", ${ca}.${child.id}::text AS "__key", count(*) OVER () AS "__total" ` +
|
|
211
|
+
`FROM ${child.name} AS ${ca} WHERE ${conds.join(" AND ")} ORDER BY ${ca}.${child.id}::text LIMIT $${params.length}`;
|
|
212
|
+
parts.push(`'${out}', (SELECT json_build_object(` +
|
|
213
|
+
`'items', coalesce(json_agg("__p"."__row" ORDER BY "__p"."__key"), '[]'::json), ` +
|
|
214
|
+
`'total', coalesce(max("__p"."__total"), 0)::int, ` +
|
|
215
|
+
`'hasMore', coalesce(count(*) < max("__p"."__total"), false), ` +
|
|
216
|
+
`'cursor', max("__p"."__key")` +
|
|
217
|
+
`) FROM (${rows}) AS "__p")`);
|
|
218
|
+
}
|
|
219
|
+
return `json_build_object(${parts.join(", ")})`;
|
|
220
|
+
}
|
|
221
|
+
/** The WHERE fragment for the read policy the runtime pushed down, or nothing when there is none to push. */
|
|
222
|
+
policyWhere(t, ctx, args, params, alias, own) {
|
|
223
|
+
const filter = own ?? ctx.policy?.filter;
|
|
224
|
+
if (!filter)
|
|
225
|
+
return [];
|
|
226
|
+
const env = { viewer: ctx.viewer ?? null, args, this: null, ...(ctx.now ? { now: ctx.now } : {}) };
|
|
227
|
+
const f = compilePolicy(filter, env, (name) => {
|
|
228
|
+
const col = t.column(name);
|
|
229
|
+
const def = t.def.fields.find((x) => x.name === name);
|
|
230
|
+
const qualified = col && alias ? `${alias}.${col}` : col;
|
|
231
|
+
return qualified && def && def.type.kind === "named" ? { column: qualified, scalar: this.scalarOf(def.type.name) } : undefined;
|
|
232
|
+
}, params);
|
|
233
|
+
return f.sql === "TRUE" ? [] : [f.sql];
|
|
234
|
+
}
|
|
235
|
+
/** The read policy declared on a type, for the levels of a screen the runtime handed no hint for. */
|
|
236
|
+
filterFor(type) {
|
|
237
|
+
const def = this.opts.ir.types[type];
|
|
238
|
+
return def ? pushableFilterOf(def.annotations) : undefined;
|
|
239
|
+
}
|
|
240
|
+
equalities(t, where, params) {
|
|
241
|
+
return Object.entries(where).map(([f, v]) => {
|
|
242
|
+
const col = t.column(f);
|
|
243
|
+
if (!col)
|
|
244
|
+
throw new Error(`@rayfold/postgres: ${t.def.name} has no field ${f}`);
|
|
245
|
+
if (v === null || v === undefined)
|
|
246
|
+
return `${col} IS NULL`;
|
|
247
|
+
params.push(v);
|
|
248
|
+
return `${col} = $${params.length}`;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async select(t, sql, params) {
|
|
252
|
+
return (await this.db.query(sql, params)).rows.map((r) => this.row(t, r));
|
|
253
|
+
}
|
|
254
|
+
row(t, r) {
|
|
255
|
+
const out = {};
|
|
256
|
+
for (const [k, v] of Object.entries(r))
|
|
257
|
+
if (k !== "__total" && k !== "__n")
|
|
258
|
+
out[t.field(k)] = v;
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
table(type) {
|
|
262
|
+
const t = this.tables.get(type);
|
|
263
|
+
if (!t)
|
|
264
|
+
throw new Error(`@rayfold/postgres: no table mapped for ${type}`);
|
|
265
|
+
return t;
|
|
266
|
+
}
|
|
267
|
+
scalarOf(name) {
|
|
268
|
+
const def = this.opts.ir.types[name];
|
|
269
|
+
return def?.kind === "enum" ? "String" : name;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// ---------------------------------------------------------------- policies to SQL
|
|
273
|
+
/** The rows of a `Page<T>` selection: the sub-shape of its `items`, or the shape itself when it selects rows. */
|
|
274
|
+
function itemsOf(shape) {
|
|
275
|
+
const items = shape.items.find((i) => i.kind === "field" && i.name === "items");
|
|
276
|
+
return items && items.kind === "field" && items.shape ? items.shape : shape;
|
|
277
|
+
}
|
|
278
|
+
/** Scalars Postgres would render as a JSON number, but Rayfold carries as text. */
|
|
279
|
+
const AS_TEXT = new Set(["Decimal", "Long"]);
|
|
280
|
+
/** The page size a shape asked for (`page: { first: n }` or `first: n`), when it is a plain number. */
|
|
281
|
+
function firstOf(a) {
|
|
282
|
+
const page = a?.["page"];
|
|
283
|
+
const n = page && typeof page === "object" && !Array.isArray(page) ? page["first"] : a?.["first"];
|
|
284
|
+
return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : undefined;
|
|
285
|
+
}
|
|
286
|
+
/** The pushable part of a type's `@allow(read:)`, for the levels of a screen the runtime gave no hint for. */
|
|
287
|
+
function pushableFilterOf(annotations) {
|
|
288
|
+
const allow = annotations.find((a) => a.name === "allow");
|
|
289
|
+
const read = allow?.args["read"];
|
|
290
|
+
return read && typeof read === "object" && "$expr" in read ? read.$expr : undefined;
|
|
291
|
+
}
|
|
292
|
+
const LOOSE = { sql: "TRUE", exact: false };
|
|
293
|
+
const constant = (b) => ({ sql: b ? "TRUE" : "FALSE", exact: true });
|
|
294
|
+
const TEXT = new Set(["String", "ID"]);
|
|
295
|
+
const NUMBER = new Set(["Int", "Float"]);
|
|
296
|
+
const DECIMAL = new Set(["Decimal", "Long"]);
|
|
297
|
+
const DECIMAL_TEXT = /^-?\d+(\.\d+)?$/;
|
|
298
|
+
/**
|
|
299
|
+
* Translates a pushable read policy (spec 06 §4) into a WHERE fragment under `env` (the viewer and args), appending
|
|
300
|
+
* its values to `params`. Parts that read no field of the row are evaluated here, as the runtime would. `exact` is true
|
|
301
|
+
* when the fragment selects exactly the rows the policy allows; otherwise it selects more, never fewer.
|
|
302
|
+
*/
|
|
303
|
+
export function compilePolicy(e, env, columnOf, params) {
|
|
304
|
+
if (!readsRow(e)) {
|
|
305
|
+
try {
|
|
306
|
+
return constant(truthy(evalExpr(e, env)));
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return LOOSE; // an expression that cannot be evaluated denies, but maybe only where it is reached: leave it to the runtime
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
switch (e.k) {
|
|
313
|
+
case "path": {
|
|
314
|
+
const c = e.path.length === 1 ? columnOf(e.path[0]) : undefined;
|
|
315
|
+
if (!c)
|
|
316
|
+
return LOOSE;
|
|
317
|
+
return { sql: c.scalar === "Boolean" ? `(${c.column} IS TRUE)` : `(${c.column} IS NOT NULL)`, exact: true };
|
|
318
|
+
}
|
|
319
|
+
case "not": {
|
|
320
|
+
const f = compilePolicy(e.e, env, columnOf, params);
|
|
321
|
+
// negating a superset would give a subset, so only an exact fragment can be negated
|
|
322
|
+
return f.exact ? { sql: `(NOT ${f.sql})`, exact: true } : LOOSE;
|
|
323
|
+
}
|
|
324
|
+
case "bin": {
|
|
325
|
+
if (e.op === "&&" || e.op === "||") {
|
|
326
|
+
const l = compilePolicy(e.l, env, columnOf, params);
|
|
327
|
+
const r = compilePolicy(e.r, env, columnOf, params);
|
|
328
|
+
return { sql: `(${l.sql} ${e.op === "&&" ? "AND" : "OR"} ${r.sql})`, exact: l.exact && r.exact };
|
|
329
|
+
}
|
|
330
|
+
const left = rowField(e.l);
|
|
331
|
+
const right = rowField(e.r);
|
|
332
|
+
if ((left === undefined) === (right === undefined))
|
|
333
|
+
return LOOSE; // both sides read the row, or neither is a plain field
|
|
334
|
+
const field = (left ?? right);
|
|
335
|
+
const other = left !== undefined ? e.r : e.l;
|
|
336
|
+
if (readsRow(other))
|
|
337
|
+
return LOOSE;
|
|
338
|
+
const c = columnOf(field);
|
|
339
|
+
if (!c)
|
|
340
|
+
return LOOSE;
|
|
341
|
+
let v;
|
|
342
|
+
try {
|
|
343
|
+
v = evalExpr(other, env);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return LOOSE;
|
|
347
|
+
}
|
|
348
|
+
const op = left !== undefined ? e.op : flip(e.op);
|
|
349
|
+
return compare(c, op, v, params);
|
|
350
|
+
}
|
|
351
|
+
default:
|
|
352
|
+
return LOOSE;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function compare(c, op, v, params) {
|
|
356
|
+
const add = (x) => `$${params.push(x)}`;
|
|
357
|
+
if (op === "in") {
|
|
358
|
+
if (!Array.isArray(v))
|
|
359
|
+
return constant(false);
|
|
360
|
+
if (!v.every((x) => kindMatches(c.scalar, x)))
|
|
361
|
+
return LOOSE;
|
|
362
|
+
if (TEXT.has(c.scalar))
|
|
363
|
+
return { sql: `COALESCE(${c.column}::text = ANY(${add(v.map(String))}::text[]), FALSE)`, exact: true };
|
|
364
|
+
if (NUMBER.has(c.scalar))
|
|
365
|
+
return { sql: `COALESCE(${c.column} = ANY(${add(v)}::numeric[]), FALSE)`, exact: true };
|
|
366
|
+
return LOOSE;
|
|
367
|
+
}
|
|
368
|
+
if (op === "==" || op === "!=") {
|
|
369
|
+
if (v === null || v === undefined)
|
|
370
|
+
return { sql: `(${c.column} IS ${op === "==" ? "" : "NOT "}NULL)`, exact: true };
|
|
371
|
+
if (!kindMatches(c.scalar, v))
|
|
372
|
+
return LOOSE;
|
|
373
|
+
const not = op === "==" ? "NOT " : "";
|
|
374
|
+
if (TEXT.has(c.scalar))
|
|
375
|
+
return { sql: `(${c.column}::text IS ${not}DISTINCT FROM ${add(String(v))}::text)`, exact: true };
|
|
376
|
+
if (NUMBER.has(c.scalar))
|
|
377
|
+
return { sql: `(${c.column} IS ${not}DISTINCT FROM ${add(v)}::numeric)`, exact: true };
|
|
378
|
+
if (c.scalar === "Boolean")
|
|
379
|
+
return { sql: `(${c.column} IS ${not}DISTINCT FROM ${add(v)}::boolean)`, exact: true };
|
|
380
|
+
// Decimal and Long travel as text: SQL compares by value, the runtime may compare the text, so equality selects
|
|
381
|
+
// more rows than the policy and inequality could select fewer; only equality is pushed
|
|
382
|
+
if (DECIMAL.has(c.scalar) && op === "==")
|
|
383
|
+
return { sql: `(${c.column} = ${add(String(v))}::numeric)`, exact: false };
|
|
384
|
+
return LOOSE;
|
|
385
|
+
}
|
|
386
|
+
if (["<", "<=", ">", ">="].includes(op) && typeof v === "number" && NUMBER.has(c.scalar)) {
|
|
387
|
+
return { sql: `COALESCE(${c.column} ${op} ${add(v)}::numeric, FALSE)`, exact: true };
|
|
388
|
+
}
|
|
389
|
+
return LOOSE;
|
|
390
|
+
}
|
|
391
|
+
/** Whether `v` is a value the runtime could find equal to a field of this scalar type. */
|
|
392
|
+
function kindMatches(scalar, v) {
|
|
393
|
+
if (TEXT.has(scalar))
|
|
394
|
+
return typeof v === "string";
|
|
395
|
+
if (NUMBER.has(scalar))
|
|
396
|
+
return typeof v === "number" && Number.isFinite(v);
|
|
397
|
+
if (scalar === "Boolean")
|
|
398
|
+
return typeof v === "boolean";
|
|
399
|
+
if (DECIMAL.has(scalar))
|
|
400
|
+
return (typeof v === "string" && DECIMAL_TEXT.test(v)) || (typeof v === "number" && Number.isFinite(v));
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
function flip(op) {
|
|
404
|
+
return op === "<" ? ">" : op === ">" ? "<" : op === "<=" ? ">=" : op === ">=" ? "<=" : op;
|
|
405
|
+
}
|
|
406
|
+
/** The field name when `e` is `this.<field>`. */
|
|
407
|
+
function rowField(e) {
|
|
408
|
+
return e.k === "path" && e.root === "this" && e.path.length === 1 ? e.path[0] : undefined;
|
|
409
|
+
}
|
|
410
|
+
function readsRow(e) {
|
|
411
|
+
switch (e.k) {
|
|
412
|
+
case "lit":
|
|
413
|
+
return false;
|
|
414
|
+
case "path":
|
|
415
|
+
return e.root === "this";
|
|
416
|
+
case "not":
|
|
417
|
+
return readsRow(e.e);
|
|
418
|
+
case "bin":
|
|
419
|
+
return readsRow(e.l) || readsRow(e.r);
|
|
420
|
+
case "list":
|
|
421
|
+
return e.items.some(readsRow);
|
|
422
|
+
case "call":
|
|
423
|
+
return e.args.some(readsRow);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function truthy(v) {
|
|
427
|
+
return v !== null && v !== undefined && v !== false;
|
|
428
|
+
}
|
|
429
|
+
function quote(name) {
|
|
430
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
431
|
+
}
|
|
432
|
+
function snake(name) {
|
|
433
|
+
return name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
434
|
+
}
|
|
435
|
+
//# sourceMappingURL=index.js.map
|
package/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Postgres for Rayfold resolvers: batch loads by id, keyset pages, one-to-many pages for a whole level in one query,\n * and read policies pushed into the SQL `WHERE` (spec 06 §4), so a list never fetches rows its viewer may not see.\n *\n * Works with any client that has `query(text, params)`: pg's `Pool` and `Client`, PGlite, or a wrapper of your own.\n * Every value travels as a bound parameter and every identifier is quoted.\n *\n * The pushed-down filter is a superset of the policy: it may let through a row the policy denies (the runtime's own\n * check then removes it), but it never drops a row the policy allows. Comparisons it cannot translate exactly, such as\n * ordering text or comparing across types, are left to the runtime.\n */\nimport { evalExpr, type Expr, type ExprEnv, type FieldDef, type RayfoldSchemaIR, type Shape } from \"@rayfold/schema\";\n\n/** Anything with pg's `query(text, params)`: `pg.Pool`, `pg.Client`, `PGlite`. */\nexport interface Queryable {\n query<R = Record<string, unknown>>(text: string, params?: unknown[]): Promise<{ rows: R[] }>;\n}\n\nexport interface TableMapping {\n /** The table, optionally schema-qualified (`\"public.books\"`). */\n table: string;\n /** The key column; default `\"id\"`. Pages are ordered by it. */\n id?: string;\n /** Field name to column name, for fields whose column is named otherwise (after `naming`). */\n columns?: Record<string, string>;\n /** Fields that lead to another mapped type. `screen` needs these; the batch loaders do not. */\n relations?: Record<string, Relation>;\n}\n\n/**\n * How a field reaches another table. `one`: `key` is the field on *this* type holding the other row's key\n * (`Book.authorId`). `page`: `key` is the field on the *other* type holding this row's key (`Book.authorId`, as seen\n * from `Author.books`).\n */\nexport interface Relation {\n type: string;\n kind: \"one\" | \"page\";\n key: string;\n}\n\nexport interface PgStoreOptions {\n ir: RayfoldSchemaIR;\n /** One mapping per entity type the store serves. */\n tables: Record<string, TableMapping>;\n /** How field names become column names when `columns` does not say: as they are (default) or snake_case. */\n naming?: \"same\" | \"snake\";\n}\n\n/** What the store needs from a resolver's context: the viewer and the read policy the runtime pushed down (`ctx.policy`). */\nexport interface PolicyContext {\n viewer?: unknown;\n policy?: { filter?: Expr };\n now?: () => number;\n}\n\nexport type Row = Record<string, unknown>;\nexport interface Page<T = Row> { items: T[]; cursor: string | null; hasMore: boolean; total: number }\nexport interface PageRequest { first: number; after?: string | null }\n\n/** A SQL fragment with its parameters numbered from 1. */\nexport interface Fragment { sql: string; exact: boolean }\n\nexport function createPgStore(db: Queryable, opts: PgStoreOptions): PgStore {\n return new PgStore(db, opts);\n}\n\ninterface Table { name: string; id: string; idField: string; column(field: string): string | undefined; field(column: string): string; def: { name: string; fields: FieldDef[] } }\n\nexport class PgStore {\n private readonly tables = new Map<string, Table>();\n\n constructor(private readonly db: Queryable, private readonly opts: PgStoreOptions) {\n for (const [type, m] of Object.entries(opts.tables)) {\n const def = opts.ir.types[type];\n if (!def || !(\"fields\" in def)) throw new Error(`@rayfold/postgres: ${type} is not an entity or object of the schema`);\n const columns = new Map<string, string>();\n for (const f of def.fields) columns.set(f.name, m.columns?.[f.name] ?? (opts.naming === \"snake\" ? snake(f.name) : f.name));\n for (const [f, c] of Object.entries(m.columns ?? {})) columns.set(f, c);\n const id = m.id ?? \"id\";\n const idField = [...columns].find(([, c]) => c === id)?.[0] ?? \"id\";\n const fields = new Map([...columns].map(([f, c]) => [c, f]));\n this.tables.set(type, {\n name: m.table.split(\".\").map(quote).join(\".\"),\n id: quote(id),\n idField,\n column: (f) => (columns.has(f) ? quote(columns.get(f)!) : undefined),\n field: (c) => fields.get(c) ?? c,\n def: { name: def.name, fields: def.fields },\n });\n }\n }\n\n /**\n * Rows of `type` for `ids`, in the order asked: `null` where a row is missing or hidden by the pushed-down read policy.\n * One query for any number of ids, which makes it the batch loader for a to-one field.\n */\n async byIds(type: string, ids: readonly unknown[], ctx: PolicyContext = {}, args: Record<string, unknown> = {}): Promise<Array<Row | null>> {\n if (!ids.length) return [];\n const t = this.table(type);\n const params: unknown[] = [[...new Set(ids.filter((x) => x !== null && x !== undefined).map(String))]];\n const where = [`${t.id}::text = ANY($1::text[])`, ...this.policyWhere(t, ctx, args, params)];\n const rows = await this.select(t, `SELECT * FROM ${t.name} WHERE ${where.join(\" AND \")}`, params);\n const byId = new Map(rows.map((r) => [String(r[t.idField]), r]));\n return ids.map((id) => (id === null || id === undefined ? null : (byId.get(String(id)) ?? null)));\n }\n\n /** Rows of `type` whose fields equal `where` (a `null` value matches NULL), in key order. One query. */\n async find(type: string, where: Record<string, unknown> = {}, ctx: PolicyContext = {}, args: Record<string, unknown> = {}): Promise<Row[]> {\n const t = this.table(type);\n const params: unknown[] = [];\n const conds = [...this.equalities(t, where, params), ...this.policyWhere(t, ctx, args, params)];\n return this.select(t, `SELECT * FROM ${t.name}${conds.length ? ` WHERE ${conds.join(\" AND \")}` : \"\"} ORDER BY ${t.id}`, params);\n }\n\n /**\n * One page of `type` in key order, after `page.after` (a key), with `total` counting every visible matching row.\n * One query; a second one counts only when the page comes back empty.\n */\n async page(type: string, page: PageRequest, where: Record<string, unknown> = {}, ctx: PolicyContext = {}, args: Record<string, unknown> = {}): Promise<Page> {\n const t = this.table(type);\n const params: unknown[] = [];\n const conds = [...this.equalities(t, where, params), ...this.policyWhere(t, ctx, args, params)];\n const filtered = `SELECT *, count(*) OVER () AS \"__total\" FROM ${t.name}${conds.length ? ` WHERE ${conds.join(\" AND \")}` : \"\"}`;\n let after = \"\";\n if (page.after) {\n params.push(page.after);\n after = ` WHERE \"__s\".${t.id}::text > $${params.length}::text`;\n }\n params.push(page.first + 1);\n const raw = (await this.db.query<Row>(`SELECT * FROM (${filtered}) AS \"__s\"${after} ORDER BY \"__s\".${t.id}::text LIMIT $${params.length}`, params)).rows;\n let total = raw.length ? Number(raw[0]![\"__total\"]) : 0;\n if (!raw.length && page.after) {\n const count = await this.db.query<{ n: number }>(`SELECT count(*)::int AS n FROM ${t.name}${conds.length ? ` WHERE ${conds.join(\" AND \")}` : \"\"}`, params.slice(0, params.length - 2));\n total = Number(count.rows[0]?.n ?? 0);\n }\n const rows = raw.map((r) => this.row(t, r));\n const items = rows.slice(0, page.first);\n return { items, cursor: items.length ? String(items[items.length - 1]![t.idField]) : null, hasMore: rows.length > page.first, total };\n }\n\n /**\n * For each parent key in `values`, the page of `type` rows whose `field` equals it: the batch loader for a paged\n * one-to-many field (an author's books). One query for the whole level, whatever the number of parents.\n */\n async pagesByField(type: string, field: string, values: readonly unknown[], page: PageRequest, ctx: PolicyContext = {}, args: Record<string, unknown> = {}): Promise<Page[]> {\n if (!values.length) return [];\n const t = this.table(type);\n const col = t.column(field);\n if (!col) throw new Error(`@rayfold/postgres: ${type} has no field ${field}`);\n const params: unknown[] = [[...new Set(values.map(String))]];\n const conds = [`${col}::text = ANY($1::text[])`, ...this.policyWhere(t, ctx, args, params)];\n let after = \"\";\n if (page.after) {\n params.push(page.after);\n after = ` AND \"__s\".${t.id}::text > $${params.length}::text`;\n }\n params.push(page.first + 1);\n const sql =\n `SELECT * FROM (SELECT *, count(*) OVER (PARTITION BY ${col}) AS \"__total\", ` +\n `row_number() OVER (PARTITION BY ${col} ORDER BY ${t.id}::text) AS \"__n\" FROM ${t.name} WHERE ${conds.join(\" AND \")}) AS \"__s\" ` +\n `WHERE TRUE${after} ORDER BY \"__s\".${t.id}::text`;\n const raw = (await this.db.query<Row>(sql, params.slice(0, -1))).rows;\n const groups = new Map<string, { rows: Row[]; total: number }>();\n for (const r of raw) {\n const key = String(r[col.slice(1, -1).replace(/\"\"/g, '\"')]);\n const g = groups.get(key) ?? { rows: [], total: Number(r[\"__total\"]) };\n g.rows.push(this.row(t, r));\n groups.set(key, g);\n }\n return values.map((v) => {\n const g = groups.get(String(v));\n if (!g) return { items: [], cursor: null, hasMore: false, total: 0 };\n const items = g.rows.slice(0, page.first);\n return { items, cursor: items.length ? String(items[items.length - 1]![t.idField]) : null, hasMore: g.rows.length > page.first, total: g.total };\n });\n }\n\n /**\n * One statement for a whole screen: a page of `type` with every nested field the shape asks for, gathered as JSON\n * by correlated subqueries. Depth costs no extra round trip, and each level's read policy is pushed into its own\n * `WHERE`, so a nested list never carries rows its viewer may not see.\n *\n * Nested pages are first pages (`first`), which is what a screen shows; the root page takes a cursor as usual.\n * Every selected field must be a mapped column or a declared relation.\n */\n async screen(type: string, shape: Shape, page: PageRequest, where: Record<string, unknown> = {}, ctx: PolicyContext = {}, args: Record<string, unknown> = {}): Promise<Page> {\n const t = this.table(type);\n const params: unknown[] = [];\n // The caller may pass the shape of the Page the query returns, or the shape of one row.\n const rowShape = t.def.fields.some((f) => f.name === \"items\") ? shape : itemsOf(shape);\n const projection = this.project(t, \"__r0\", rowShape, ctx, args, params, 0);\n const conds = [...this.equalities(t, where, params), ...this.policyWhere(t, ctx, args, params, \"__r0\", ctx.policy?.filter ?? this.filterFor(type))];\n const filtered =\n `SELECT ${projection} AS \"__row\", __r0.${t.id}::text AS \"__key\", count(*) OVER () AS \"__total\" ` +\n `FROM ${t.name} AS __r0${conds.length ? ` WHERE ${conds.join(\" AND \")}` : \"\"}`;\n let after = \"\";\n if (page.after) {\n params.push(page.after);\n after = ` WHERE \"__s\".\"__key\" > $${params.length}::text`;\n }\n params.push(page.first + 1);\n const raw = (await this.db.query<Row>(`SELECT * FROM (${filtered}) AS \"__s\"${after} ORDER BY \"__s\".\"__key\" LIMIT $${params.length}`, params)).rows;\n let total = raw.length ? Number(raw[0]![\"__total\"]) : 0;\n if (!raw.length && page.after) {\n const cp: unknown[] = [];\n const cc = [...this.equalities(t, where, cp), ...this.policyWhere(t, ctx, args, cp, \"__r0\", ctx.policy?.filter ?? this.filterFor(type))];\n const counted = await this.db.query<{ n: number }>(`SELECT count(*)::int AS n FROM ${t.name} AS __r0${cc.length ? ` WHERE ${cc.join(\" AND \")}` : \"\"}`, cp);\n total = Number(counted.rows[0]?.n ?? 0);\n }\n const rows = raw.slice(0, page.first);\n return {\n items: rows.map((r) => r[\"__row\"] as Row),\n cursor: rows.length ? String(rows[rows.length - 1]![\"__key\"]) : null,\n hasMore: raw.length > page.first,\n total,\n };\n }\n\n /** The JSON projection of one level of a screen: scalars from columns, relations from correlated subqueries. */\n private project(t: Table, alias: string, shape: Shape, ctx: PolicyContext, args: Record<string, unknown>, params: unknown[], depth: number): string {\n if (depth > 8) throw new Error(\"@rayfold/postgres: a screen nested deeper than 8 levels\");\n const relations = this.opts.tables[t.def.name]?.relations ?? {};\n const parts: string[] = [];\n for (const item of shape.items) {\n if (item.kind !== \"field\") continue; // views and type conditions are expanded before a shape reaches the store\n const name = item.name;\n const out = item.alias ?? name;\n const def = t.def.fields.find((f) => f.name === name);\n if (!def) throw new Error(`@rayfold/postgres: ${t.def.name} has no field ${name}`);\n const rel = relations[name];\n if (!rel) {\n const col = t.column(name);\n if (!col) throw new Error(`@rayfold/postgres: ${t.def.name}.${name} has no column and no relation`);\n const scalar = def.type.kind === \"named\" ? this.scalarOf(def.type.name) : \"\";\n parts.push(`'${out}', ${alias}.${col}${AS_TEXT.has(scalar) ? \"::text\" : \"\"}`);\n continue;\n }\n const child = this.table(rel.type);\n const ca = `__r${depth + 1}`;\n // A paged field is selected through its Page envelope, so the rows are described by its `items`.\n const selected = item.shape ?? { items: [] };\n const childProjection = this.project(child, ca, rel.kind === \"page\" ? itemsOf(selected) : selected, ctx, args, params, depth + 1);\n const childPolicy = this.policyWhere(child, ctx, args, params, ca, this.filterFor(rel.type));\n if (rel.kind === \"one\") {\n const parentCol = t.column(rel.key);\n if (!parentCol) throw new Error(`@rayfold/postgres: ${t.def.name} has no field ${rel.key}`);\n const conds = [`${ca}.${child.id} = ${alias}.${parentCol}`, ...childPolicy];\n parts.push(`'${out}', (SELECT ${childProjection} FROM ${child.name} AS ${ca} WHERE ${conds.join(\" AND \")} LIMIT 1)`);\n continue;\n }\n const childCol = child.column(rel.key);\n if (!childCol) throw new Error(`@rayfold/postgres: ${rel.type} has no field ${rel.key}`);\n params.push(firstOf(item.args) ?? 10);\n const conds = [`${ca}.${childCol} = ${alias}.${t.id}`, ...childPolicy];\n const rows =\n `SELECT ${childProjection} AS \"__row\", ${ca}.${child.id}::text AS \"__key\", count(*) OVER () AS \"__total\" ` +\n `FROM ${child.name} AS ${ca} WHERE ${conds.join(\" AND \")} ORDER BY ${ca}.${child.id}::text LIMIT $${params.length}`;\n parts.push(\n `'${out}', (SELECT json_build_object(` +\n `'items', coalesce(json_agg(\"__p\".\"__row\" ORDER BY \"__p\".\"__key\"), '[]'::json), ` +\n `'total', coalesce(max(\"__p\".\"__total\"), 0)::int, ` +\n `'hasMore', coalesce(count(*) < max(\"__p\".\"__total\"), false), ` +\n `'cursor', max(\"__p\".\"__key\")` +\n `) FROM (${rows}) AS \"__p\")`,\n );\n }\n return `json_build_object(${parts.join(\", \")})`;\n }\n\n /** The WHERE fragment for the read policy the runtime pushed down, or nothing when there is none to push. */\n private policyWhere(t: Table, ctx: PolicyContext, args: Record<string, unknown>, params: unknown[], alias?: string, own?: Expr): string[] {\n const filter = own ?? ctx.policy?.filter;\n if (!filter) return [];\n const env: ExprEnv = { viewer: ctx.viewer ?? null, args, this: null, ...(ctx.now ? { now: ctx.now } : {}) };\n const f = compilePolicy(filter, env, (name) => {\n const col = t.column(name);\n const def = t.def.fields.find((x) => x.name === name);\n const qualified = col && alias ? `${alias}.${col}` : col;\n return qualified && def && def.type.kind === \"named\" ? { column: qualified, scalar: this.scalarOf(def.type.name) } : undefined;\n }, params);\n return f.sql === \"TRUE\" ? [] : [f.sql];\n }\n\n /** The read policy declared on a type, for the levels of a screen the runtime handed no hint for. */\n private filterFor(type: string): Expr | undefined {\n const def = this.opts.ir.types[type];\n return def ? pushableFilterOf(def.annotations) : undefined;\n }\n\n private equalities(t: Table, where: Record<string, unknown>, params: unknown[]): string[] {\n return Object.entries(where).map(([f, v]) => {\n const col = t.column(f);\n if (!col) throw new Error(`@rayfold/postgres: ${t.def.name} has no field ${f}`);\n if (v === null || v === undefined) return `${col} IS NULL`;\n params.push(v);\n return `${col} = $${params.length}`;\n });\n }\n\n private async select(t: Table, sql: string, params: unknown[]): Promise<Row[]> {\n return (await this.db.query<Row>(sql, params)).rows.map((r) => this.row(t, r));\n }\n\n private row(t: Table, r: Row): Row {\n const out: Row = {};\n for (const [k, v] of Object.entries(r)) if (k !== \"__total\" && k !== \"__n\") out[t.field(k)] = v;\n return out;\n }\n\n private table(type: string): Table {\n const t = this.tables.get(type);\n if (!t) throw new Error(`@rayfold/postgres: no table mapped for ${type}`);\n return t;\n }\n\n private scalarOf(name: string): string {\n const def = this.opts.ir.types[name];\n return def?.kind === \"enum\" ? \"String\" : name;\n }\n}\n\n// ---------------------------------------------------------------- policies to SQL\n\n/** The rows of a `Page<T>` selection: the sub-shape of its `items`, or the shape itself when it selects rows. */\nfunction itemsOf(shape: Shape): Shape {\n const items = shape.items.find((i) => i.kind === \"field\" && i.name === \"items\");\n return items && items.kind === \"field\" && items.shape ? items.shape : shape;\n}\n\n/** Scalars Postgres would render as a JSON number, but Rayfold carries as text. */\nconst AS_TEXT = new Set([\"Decimal\", \"Long\"]);\n\n/** The page size a shape asked for (`page: { first: n }` or `first: n`), when it is a plain number. */\nfunction firstOf(a: Record<string, unknown> | undefined): number | undefined {\n const page = a?.[\"page\"];\n const n = page && typeof page === \"object\" && !Array.isArray(page) ? (page as Record<string, unknown>)[\"first\"] : a?.[\"first\"];\n return typeof n === \"number\" && Number.isInteger(n) && n >= 0 ? n : undefined;\n}\n\n/** The pushable part of a type's `@allow(read:)`, for the levels of a screen the runtime gave no hint for. */\nfunction pushableFilterOf(annotations: Array<{ name: string; args: Record<string, unknown> }>): Expr | undefined {\n const allow = annotations.find((a) => a.name === \"allow\");\n const read = allow?.args[\"read\"];\n return read && typeof read === \"object\" && \"$expr\" in read ? (read as { $expr: Expr }).$expr : undefined;\n}\n\n/** A column a policy may read, with the Rayfold scalar type of its field. */\nexport interface PolicyColumn { column: string; scalar: string }\n\nconst LOOSE: Fragment = { sql: \"TRUE\", exact: false };\nconst constant = (b: boolean): Fragment => ({ sql: b ? \"TRUE\" : \"FALSE\", exact: true });\nconst TEXT = new Set([\"String\", \"ID\"]);\nconst NUMBER = new Set([\"Int\", \"Float\"]);\nconst DECIMAL = new Set([\"Decimal\", \"Long\"]);\nconst DECIMAL_TEXT = /^-?\\d+(\\.\\d+)?$/;\n\n/**\n * Translates a pushable read policy (spec 06 §4) into a WHERE fragment under `env` (the viewer and args), appending\n * its values to `params`. Parts that read no field of the row are evaluated here, as the runtime would. `exact` is true\n * when the fragment selects exactly the rows the policy allows; otherwise it selects more, never fewer.\n */\nexport function compilePolicy(e: Expr, env: ExprEnv, columnOf: (field: string) => PolicyColumn | undefined, params: unknown[]): Fragment {\n if (!readsRow(e)) {\n try {\n return constant(truthy(evalExpr(e, env)));\n } catch {\n return LOOSE; // an expression that cannot be evaluated denies, but maybe only where it is reached: leave it to the runtime\n }\n }\n switch (e.k) {\n case \"path\": {\n const c = e.path.length === 1 ? columnOf(e.path[0]!) : undefined;\n if (!c) return LOOSE;\n return { sql: c.scalar === \"Boolean\" ? `(${c.column} IS TRUE)` : `(${c.column} IS NOT NULL)`, exact: true };\n }\n case \"not\": {\n const f = compilePolicy(e.e, env, columnOf, params);\n // negating a superset would give a subset, so only an exact fragment can be negated\n return f.exact ? { sql: `(NOT ${f.sql})`, exact: true } : LOOSE;\n }\n case \"bin\": {\n if (e.op === \"&&\" || e.op === \"||\") {\n const l = compilePolicy(e.l, env, columnOf, params);\n const r = compilePolicy(e.r, env, columnOf, params);\n return { sql: `(${l.sql} ${e.op === \"&&\" ? \"AND\" : \"OR\"} ${r.sql})`, exact: l.exact && r.exact };\n }\n const left = rowField(e.l);\n const right = rowField(e.r);\n if ((left === undefined) === (right === undefined)) return LOOSE; // both sides read the row, or neither is a plain field\n const field = (left ?? right)!;\n const other = left !== undefined ? e.r : e.l;\n if (readsRow(other)) return LOOSE;\n const c = columnOf(field);\n if (!c) return LOOSE;\n let v: unknown;\n try {\n v = evalExpr(other, env);\n } catch {\n return LOOSE;\n }\n const op = left !== undefined ? e.op : flip(e.op);\n return compare(c, op, v, params);\n }\n default:\n return LOOSE;\n }\n}\n\nfunction compare(c: PolicyColumn, op: string, v: unknown, params: unknown[]): Fragment {\n const add = (x: unknown) => `$${params.push(x)}`;\n if (op === \"in\") {\n if (!Array.isArray(v)) return constant(false);\n if (!v.every((x) => kindMatches(c.scalar, x))) return LOOSE;\n if (TEXT.has(c.scalar)) return { sql: `COALESCE(${c.column}::text = ANY(${add(v.map(String))}::text[]), FALSE)`, exact: true };\n if (NUMBER.has(c.scalar)) return { sql: `COALESCE(${c.column} = ANY(${add(v)}::numeric[]), FALSE)`, exact: true };\n return LOOSE;\n }\n if (op === \"==\" || op === \"!=\") {\n if (v === null || v === undefined) return { sql: `(${c.column} IS ${op === \"==\" ? \"\" : \"NOT \"}NULL)`, exact: true };\n if (!kindMatches(c.scalar, v)) return LOOSE;\n const not = op === \"==\" ? \"NOT \" : \"\";\n if (TEXT.has(c.scalar)) return { sql: `(${c.column}::text IS ${not}DISTINCT FROM ${add(String(v))}::text)`, exact: true };\n if (NUMBER.has(c.scalar)) return { sql: `(${c.column} IS ${not}DISTINCT FROM ${add(v)}::numeric)`, exact: true };\n if (c.scalar === \"Boolean\") return { sql: `(${c.column} IS ${not}DISTINCT FROM ${add(v)}::boolean)`, exact: true };\n // Decimal and Long travel as text: SQL compares by value, the runtime may compare the text, so equality selects\n // more rows than the policy and inequality could select fewer; only equality is pushed\n if (DECIMAL.has(c.scalar) && op === \"==\") return { sql: `(${c.column} = ${add(String(v))}::numeric)`, exact: false };\n return LOOSE;\n }\n if ([\"<\", \"<=\", \">\", \">=\"].includes(op) && typeof v === \"number\" && NUMBER.has(c.scalar)) {\n return { sql: `COALESCE(${c.column} ${op} ${add(v)}::numeric, FALSE)`, exact: true };\n }\n return LOOSE;\n}\n\n/** Whether `v` is a value the runtime could find equal to a field of this scalar type. */\nfunction kindMatches(scalar: string, v: unknown): boolean {\n if (TEXT.has(scalar)) return typeof v === \"string\";\n if (NUMBER.has(scalar)) return typeof v === \"number\" && Number.isFinite(v);\n if (scalar === \"Boolean\") return typeof v === \"boolean\";\n if (DECIMAL.has(scalar)) return (typeof v === \"string\" && DECIMAL_TEXT.test(v)) || (typeof v === \"number\" && Number.isFinite(v));\n return false;\n}\n\nfunction flip(op: string): string {\n return op === \"<\" ? \">\" : op === \">\" ? \"<\" : op === \"<=\" ? \">=\" : op === \">=\" ? \"<=\" : op;\n}\n\n/** The field name when `e` is `this.<field>`. */\nfunction rowField(e: Expr): string | undefined {\n return e.k === \"path\" && e.root === \"this\" && e.path.length === 1 ? e.path[0] : undefined;\n}\n\nfunction readsRow(e: Expr): boolean {\n switch (e.k) {\n case \"lit\":\n return false;\n case \"path\":\n return e.root === \"this\";\n case \"not\":\n return readsRow(e.e);\n case \"bin\":\n return readsRow(e.l) || readsRow(e.r);\n case \"list\":\n return e.items.some(readsRow);\n case \"call\":\n return e.args.some(readsRow);\n }\n}\n\nfunction truthy(v: unknown): boolean {\n return v !== null && v !== undefined && v !== false;\n}\n\nfunction quote(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\nfunction snake(name: string): string {\n return name.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rayfold/postgres",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Postgres for Rayfold resolvers: batch loads, keyset pages, one query per level, and read policies pushed into SQL.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"rayfold",
|
|
7
|
+
"api",
|
|
8
|
+
"protocol",
|
|
9
|
+
"postgres",
|
|
10
|
+
"sql",
|
|
11
|
+
"batch",
|
|
12
|
+
"authorization"
|
|
13
|
+
],
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"homepage": "https://eddyboutros.github.io/rayfold/",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/eddyboutros/rayfold/issues"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/eddyboutros/rayfold.git",
|
|
22
|
+
"directory": "packages/postgres"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
},
|
|
29
|
+
"main": "./index.js",
|
|
30
|
+
"types": "./index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./index.d.ts",
|
|
34
|
+
"default": "./index.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@rayfold/schema": "^0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|