orchid-graphql 0.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Ilya Semenov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,404 @@
1
+ # orchid-graphql
2
+
3
+ A helper library to resolve GraphQL queries directly with [Orchid ORM](https://orchid-orm.netlify.app) tables and relations.
4
+
5
+ This is a fork of [objection-graphql-resolver](https://github.com/IlyaSemenov/objection-graphql-resolver) which does the same for [Objection.js ORM](https://vincit.github.io/objection.js/).
6
+
7
+ ## Features
8
+
9
+ - Highly effective: selects only requested fields and relations.
10
+ - Unlimited nested resolvers.
11
+ - Pagination.
12
+ - Filters like `{ date: "2020-10-01", category__in: ["News", "Politics"] }`.
13
+ - Hook into (sub)queries with query modifiers.
14
+ - Hook into field results to restrict access to sensitive information.
15
+
16
+ ## Status and limitations
17
+
18
+ This is pre-release and not battle tested. `orchid-graphql` is a quick fork of `objection-graphql-resolver`.
19
+
20
+ In particular (unlike its sister project) `orchid-graphql` currently doesn't handle types well and returns `any` most of the time.
21
+
22
+ ## Install
23
+
24
+ ```sh
25
+ npm i orchid-graphql
26
+ ```
27
+
28
+ ## Minimal all-in-one example
29
+
30
+ Run GraphQL server:
31
+
32
+ ```ts
33
+ // Everything is put into a single file for demonstration purposes.
34
+ //
35
+ // In real projects, you will want to separate tables, typedefs,
36
+ // resolvers, and the server into their own modules.
37
+
38
+ import { ApolloServer, ApolloServerOptions } from "@apollo/server"
39
+ import { startStandaloneServer } from "@apollo/server/standalone"
40
+ import gql from "graphql-tag"
41
+ import * as r from "orchid-graphql"
42
+ import { createBaseTable, orchidORM } from "orchid-orm"
43
+
44
+ // Define database tables
45
+
46
+ const BaseTable = createBaseTable()
47
+
48
+ class PostTable extends BaseTable {
49
+ readonly table = "post"
50
+ columns = this.setColumns((t) => ({
51
+ id: t.identity().primaryKey(),
52
+ text: t.text(0, 5000),
53
+ }))
54
+ }
55
+
56
+ const db = orchidORM(
57
+ {
58
+ databaseURL: process.env.DATABASE_URL,
59
+ log: true,
60
+ },
61
+ {
62
+ post: PostTable,
63
+ }
64
+ )
65
+
66
+ await db.$adapter.query(`
67
+ create table post (
68
+ id serial primary key,
69
+ text text not null
70
+ );
71
+ `)
72
+
73
+ // Define GraphQL schema
74
+
75
+ const typeDefs = gql`
76
+ type Post {
77
+ id: Int!
78
+ text: String!
79
+ }
80
+
81
+ type Mutation {
82
+ create_post(text: String!): Post!
83
+ }
84
+
85
+ type Query {
86
+ posts: [Post!]!
87
+ }
88
+ `
89
+
90
+ // Map GraphQL types to table resolvers
91
+
92
+ const graph = r.graph({
93
+ Post: r.table(db.post),
94
+ })
95
+
96
+ // Define resolvers
97
+
98
+ const resolvers: ApolloServerOptions<any>["resolvers"] = {
99
+ Mutation: {
100
+ async create_post(_parent, args, context, info) {
101
+ const post = await db.post.create(args)
102
+ return await graph.resolve(db.post.find(post.id), { context, info })
103
+ },
104
+ },
105
+ Query: {
106
+ async posts(_parent, _args, context, info) {
107
+ return await graph.resolve(db.post, { context, info })
108
+ },
109
+ },
110
+ }
111
+
112
+ // Start GraphQL server
113
+
114
+ const server = new ApolloServer({ typeDefs, resolvers })
115
+ const { url } = await startStandaloneServer(server, {
116
+ listen: { port: 4000 },
117
+ })
118
+ console.log(`Listening on ${url}`)
119
+ ```
120
+
121
+ Query it with GraphQL client:
122
+
123
+ ```ts
124
+ import { GraphQLClient } from "graphql-request"
125
+ import gql from "graphql-tag"
126
+
127
+ const client = new GraphQLClient("http://127.0.0.1:4000")
128
+
129
+ await client.request(
130
+ gql`
131
+ mutation create_post($text: String!) {
132
+ new_post: create_post(text: $text) {
133
+ id
134
+ }
135
+ }
136
+ `,
137
+ { text: "Hello, world!" }
138
+ )
139
+
140
+ const { posts } = await client.request(
141
+ gql`
142
+ query {
143
+ posts {
144
+ id
145
+ text
146
+ }
147
+ }
148
+ `
149
+ )
150
+
151
+ console.log(posts)
152
+ ```
153
+
154
+ ## Relations
155
+
156
+ Relations will be fetched automatically when resolving nested fields.
157
+
158
+ Example:
159
+
160
+ ```ts
161
+ const graph = r.graph({
162
+ User: r.type(db.user),
163
+ Post: r.type(db.post),
164
+ })
165
+ ```
166
+
167
+ ```gql
168
+ query posts_with_author {
169
+ posts {
170
+ id
171
+ text
172
+ # will use subquery if requested
173
+ author {
174
+ name
175
+ }
176
+ }
177
+ }
178
+
179
+ query user_with_posts {
180
+ user(id: ID!) {
181
+ name
182
+ # will use subquery if requested
183
+ posts {
184
+ id
185
+ text
186
+ }
187
+ }
188
+ }
189
+ ```
190
+
191
+ [More details and examples for relations.](docs/relations.md)
192
+
193
+ ## Fields access
194
+
195
+ Access to individual fields can be limited:
196
+
197
+ ```ts
198
+ const graph = r.graph({
199
+ User: r.type(db.user, {
200
+ fields: {
201
+ id: true,
202
+ name: true,
203
+ // other fields not specified here, such as user password,
204
+ // will not be accessible
205
+ },
206
+ }),
207
+ })
208
+ ```
209
+
210
+ This API also allows to fine-tune field selectors, see [API](#api) section below.
211
+
212
+ ## Pagination
213
+
214
+ Root queries and -to-many nested relations can be paginated.
215
+
216
+ ```ts
217
+ const graph = r.graph({
218
+ User: r.type(db.user, {
219
+ fields: {
220
+ id: true,
221
+ name: true,
222
+ // user.posts will be a page with nodes and continuation cursor
223
+ posts: r.page(r.cursor({ fields: ["-id"], take: 10 })),
224
+ },
225
+ }),
226
+ Post: r.type(db.post),
227
+ })
228
+ ```
229
+
230
+ To paginate root query, use:
231
+
232
+ ```ts
233
+ const resolvers = {
234
+ Query: {
235
+ posts: async (parent, args, context, info) => {
236
+ return await graph.resolvePage(
237
+ db.post,
238
+ r.cursor({ take: 10, fields: ["-id"] }),
239
+ { ctx, info }
240
+ )
241
+ },
242
+ },
243
+ }
244
+ ```
245
+
246
+ [More details and examples for pagination.](docs/pagination.md)
247
+
248
+ ## Filters
249
+
250
+ Both root and nested queries can be filtered with GraphQL arguments:
251
+
252
+ ```gql
253
+ query {
254
+ posts(filter: { date: "2020-10-01", author_id__in: [123, 456] }) {
255
+ id
256
+ date
257
+ text
258
+ author {
259
+ id
260
+ name
261
+ }
262
+ }
263
+ }
264
+ ```
265
+
266
+ Filters will run against database fields, or call field modifiers.
267
+
268
+ [More details and examples for filters.](docs/filters.md)
269
+
270
+ ## API
271
+
272
+ ```ts
273
+ import * as r from "orchid-graphql"
274
+
275
+ const graph = r.graph(
276
+ // Map GraphQL types to table resolvers (required)
277
+ {
278
+ Post: r.table(
279
+ // orchid-orm bound table (required)
280
+ db.post,
281
+ // Table resolver options
282
+ {
283
+ // List fields that can be accessed via GraphQL
284
+ // if not provided, all fields can be accessed
285
+ fields: {
286
+ // Select field from database
287
+ id: true,
288
+ // Descend into relation
289
+ // (related table must be also registered in this graph resolver)
290
+ author: true,
291
+ // Modify query when this field is resolved
292
+ preview: (q) => q.select({ preview: q.raw("substr(text,1,100)") }),
293
+ // Same as text: true
294
+ text: r.field(),
295
+ // Custom field resolver
296
+ text2: r.field({
297
+ // Table field, if different from GraphQL field
298
+ tableField: "text",
299
+ }),
300
+ preview2: r.field({
301
+ // Modify query
302
+ modify: (query) =>
303
+ query.select(raw("substr(text,1,100) as preview2")),
304
+ // Post-process selected value
305
+ transform(
306
+ // Selected value
307
+ preview,
308
+ // Current instance
309
+ post,
310
+ // Query context
311
+ context
312
+ ) {
313
+ if (preview.length < 100) {
314
+ return preview
315
+ } else {
316
+ return preview + "..."
317
+ }
318
+ },
319
+ }),
320
+ // Select all objects in -to-many relation
321
+ comments: true,
322
+ // Select all objects in -to-many relation
323
+ all_comments: r.relation({
324
+ // Table field, if different from GraphQL field
325
+ tableField: "comments",
326
+ // Enable filters on -to-many relation
327
+ filters: true,
328
+ // Modify subquery
329
+ modify: (q, { liked }) => q.where({ liked }).order({ id: "DESC" }),
330
+ // Post-process selected values, see r.field()
331
+ // transform: ...,
332
+ }),
333
+ // Paginate subquery in -to-many relation
334
+ comments_page: r.page(
335
+ // Paginator
336
+ r.cursor(
337
+ // Pagination options
338
+ // Default: { fields: ["id"], take: 10 }
339
+ {
340
+ // Which fields to use for ordering
341
+ // Prefix with - for descending sort
342
+ fields: ["name", "-id"],
343
+ // How many object to take per page
344
+ take: 10,
345
+ }
346
+ ),
347
+ {
348
+ // All r.relation() options, such as:
349
+ tableField: "comments",
350
+ }
351
+ ),
352
+ },
353
+ // Modify all queries to this table
354
+ modify: (q, { args }) => q.where(args).order({ id: "DESC" }),
355
+ // Allow all fields (`fields` will be used for overrides)
356
+ allowAllFields: true,
357
+ // Allow filters in all relations
358
+ allowAllFilters: true,
359
+ }
360
+ ),
361
+ },
362
+ // Graph options
363
+ {
364
+ // Allow all fields in all tables (`fields` will be used for overrides)
365
+ allowAllFields: true,
366
+ // Allow filters in all relations of all tables
367
+ allowAllFilters: true,
368
+ }
369
+ )
370
+
371
+ const resolvers = {
372
+ Query: {
373
+ posts: async (parent, args, context, info) => {
374
+ return await graph.resolve(
375
+ // Root query (required)
376
+ db.post,
377
+ // Options (required)
378
+ {
379
+ // Resolver context
380
+ context,
381
+ // GraphQLResolveInfo object, as passed by GraphQL executor (required)
382
+ info,
383
+ // Enable filters
384
+ filters: true,
385
+ }
386
+ )
387
+ },
388
+ posts_page: (parent, args, context, info) => {
389
+ return await graph.resolvePage(
390
+ // Root query (required)
391
+ db.post,
392
+ // Paginator (required)
393
+ r.cursor({ fields: ["-id"], take: 10 }),
394
+ // Options (required) - see graph.resolve
395
+ {
396
+ context,
397
+ info,
398
+ filters: true,
399
+ }
400
+ )
401
+ },
402
+ },
403
+ }
404
+ ```