@subsquid/openreader 2.0.0 → 3.0.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.
Files changed (100) hide show
  1. package/lib/context.d.ts +5 -2
  2. package/lib/context.d.ts.map +1 -1
  3. package/lib/ir/args.d.ts +1 -1
  4. package/lib/ir/args.d.ts.map +1 -1
  5. package/lib/ir/connection.d.ts +3 -4
  6. package/lib/ir/connection.d.ts.map +1 -1
  7. package/lib/ir/connection.js.map +1 -1
  8. package/lib/ir/fields.d.ts +6 -2
  9. package/lib/ir/fields.d.ts.map +1 -1
  10. package/lib/ir/fields.js +15 -0
  11. package/lib/ir/fields.js.map +1 -1
  12. package/lib/limit.size.d.ts +7 -2
  13. package/lib/limit.size.d.ts.map +1 -1
  14. package/lib/limit.size.js +106 -12
  15. package/lib/limit.size.js.map +1 -1
  16. package/lib/main.js +6 -9
  17. package/lib/main.js.map +1 -1
  18. package/lib/model.d.ts +3 -1
  19. package/lib/model.d.ts.map +1 -1
  20. package/lib/model.schema.d.ts +2 -2
  21. package/lib/model.schema.d.ts.map +1 -1
  22. package/lib/model.schema.js +29 -7
  23. package/lib/model.schema.js.map +1 -1
  24. package/lib/model.tools.d.ts +6 -1
  25. package/lib/model.tools.d.ts.map +1 -1
  26. package/lib/model.tools.js +111 -8
  27. package/lib/model.tools.js.map +1 -1
  28. package/lib/opencrud/orderBy.d.ts +2 -2
  29. package/lib/opencrud/orderBy.d.ts.map +1 -1
  30. package/lib/opencrud/orderBy.js +13 -17
  31. package/lib/opencrud/orderBy.js.map +1 -1
  32. package/lib/opencrud/schema.d.ts +4 -4
  33. package/lib/opencrud/schema.d.ts.map +1 -1
  34. package/lib/opencrud/schema.js +60 -64
  35. package/lib/opencrud/schema.js.map +1 -1
  36. package/lib/opencrud/tree.d.ts +9 -7
  37. package/lib/opencrud/tree.d.ts.map +1 -1
  38. package/lib/opencrud/tree.js +32 -14
  39. package/lib/opencrud/tree.js.map +1 -1
  40. package/lib/server.d.ts +16 -12
  41. package/lib/server.d.ts.map +1 -1
  42. package/lib/server.js +29 -4
  43. package/lib/server.js.map +1 -1
  44. package/lib/sql/cursor.js +2 -2
  45. package/lib/sql/cursor.js.map +1 -1
  46. package/lib/sql/mapping.d.ts +3 -1
  47. package/lib/sql/mapping.d.ts.map +1 -1
  48. package/lib/sql/mapping.js +16 -1
  49. package/lib/sql/mapping.js.map +1 -1
  50. package/lib/sql/printer.d.ts +29 -11
  51. package/lib/sql/printer.d.ts.map +1 -1
  52. package/lib/sql/printer.js +106 -10
  53. package/lib/sql/printer.js.map +1 -1
  54. package/lib/sql/query.d.ts +11 -11
  55. package/lib/sql/query.d.ts.map +1 -1
  56. package/lib/sql/query.js +41 -19
  57. package/lib/sql/query.js.map +1 -1
  58. package/lib/test/limits.test.d.ts +2 -0
  59. package/lib/test/limits.test.d.ts.map +1 -0
  60. package/lib/test/limits.test.js +159 -0
  61. package/lib/test/limits.test.js.map +1 -0
  62. package/lib/test/queryable.test.d.ts +2 -0
  63. package/lib/test/queryable.test.d.ts.map +1 -0
  64. package/lib/test/queryable.test.js +255 -0
  65. package/lib/test/queryable.test.js.map +1 -0
  66. package/lib/test/setup.d.ts +2 -1
  67. package/lib/test/setup.d.ts.map +1 -1
  68. package/lib/test/setup.js +5 -2
  69. package/lib/test/setup.js.map +1 -1
  70. package/lib/util/execute.d.ts +5 -0
  71. package/lib/util/execute.d.ts.map +1 -0
  72. package/lib/util/execute.js +28 -0
  73. package/lib/util/execute.js.map +1 -0
  74. package/lib/util/limit.d.ts +11 -0
  75. package/lib/util/limit.d.ts.map +1 -0
  76. package/lib/util/limit.js +39 -0
  77. package/lib/util/limit.js.map +1 -0
  78. package/package.json +3 -3
  79. package/src/context.ts +5 -2
  80. package/src/ir/args.ts +1 -1
  81. package/src/ir/connection.ts +3 -4
  82. package/src/ir/fields.ts +22 -2
  83. package/src/limit.size.ts +122 -13
  84. package/src/main.ts +18 -20
  85. package/src/model.schema.ts +40 -13
  86. package/src/model.tools.ts +121 -8
  87. package/src/model.ts +3 -1
  88. package/src/opencrud/orderBy.ts +13 -17
  89. package/src/opencrud/schema.ts +86 -85
  90. package/src/opencrud/tree.ts +55 -26
  91. package/src/server.ts +66 -26
  92. package/src/sql/cursor.ts +2 -2
  93. package/src/sql/mapping.ts +18 -1
  94. package/src/sql/printer.ts +137 -21
  95. package/src/sql/query.ts +50 -30
  96. package/src/test/limits.test.ts +163 -0
  97. package/src/test/queryable.test.ts +258 -0
  98. package/src/test/setup.ts +6 -3
  99. package/src/util/execute.ts +53 -0
  100. package/src/util/limit.ts +34 -0
@@ -1,29 +1,30 @@
1
- import {unexpectedCase} from "@subsquid/util-internal"
2
- import assert from "assert"
3
- import {GraphQLSchema} from "graphql"
4
- import {ResolveTree} from "graphql-parse-resolve-info"
5
- import {EntityListArguments} from "../ir/args"
6
- import {FieldRequest, OpaqueRequest} from "../ir/fields"
7
- import {Model} from "../model"
8
- import {simplifyResolveTree} from "../util/resolve-tree"
9
- import {ensureArray} from "../util/util"
10
- import {parseOrderBy} from "./orderBy"
11
- import {parseWhere} from "./where"
12
-
13
-
14
- export function parseResolveTree(
1
+ import {unexpectedCase} from '@subsquid/util-internal'
2
+ import assert from 'assert'
3
+ import {GraphQLSchema} from 'graphql'
4
+ import {ResolveTree} from 'graphql-parse-resolve-info'
5
+ import {SqlArguments} from '../ir/args'
6
+ import {AnyFields, FieldRequest, FieldsByEntity, OpaqueRequest} from '../ir/fields'
7
+ import {Model} from '../model'
8
+ import {getQueryableEntities} from '../model.tools'
9
+ import {simplifyResolveTree} from '../util/resolve-tree'
10
+ import {ensureArray} from '../util/util'
11
+ import {parseOrderBy} from './orderBy'
12
+ import {parseWhere} from './where'
13
+
14
+
15
+ export function parseObjectTree(
15
16
  model: Model,
16
- objectName: string,
17
+ typeName: string,
17
18
  schema: GraphQLSchema,
18
19
  tree: ResolveTree
19
20
  ): FieldRequest[] {
20
21
 
21
22
  let requests: FieldRequest[] = []
22
23
  let requestedScalars: Record<string, true> = {}
23
- let object = model[objectName]
24
+ let object = model[typeName]
24
25
  assert(object.kind == "entity" || object.kind == "object")
25
26
 
26
- let fields = simplifyResolveTree(schema, tree, objectName).fields
27
+ let fields = simplifyResolveTree(schema, tree, typeName).fields
27
28
  for (let alias in fields) {
28
29
  let f = fields[alias]
29
30
  let prop = object.properties[f.name]
@@ -51,7 +52,7 @@ export function parseResolveTree(
51
52
  type: prop.type,
52
53
  prop,
53
54
  index: 0,
54
- children: parseResolveTree(model, prop.type.name, schema, f)
55
+ children: parseObjectTree(model, prop.type.name, schema, f)
55
56
  })
56
57
  break
57
58
  case "union": {
@@ -59,7 +60,7 @@ export function parseResolveTree(
59
60
  assert(union.kind == "union")
60
61
  let children: FieldRequest[] = []
61
62
  for (let variant of union.variants) {
62
- for (let req of parseResolveTree(model, variant, schema, f)) {
63
+ for (let req of parseObjectTree(model, variant, schema, f)) {
63
64
  req.ifType = variant
64
65
  children.push(req)
65
66
  }
@@ -83,7 +84,7 @@ export function parseResolveTree(
83
84
  type: prop.type,
84
85
  prop,
85
86
  index: 0,
86
- children: parseResolveTree(model, prop.type.foreignEntity, schema, f)
87
+ children: parseObjectTree(model, prop.type.entity, schema, f)
87
88
  })
88
89
  break
89
90
  case "lookup":
@@ -94,7 +95,7 @@ export function parseResolveTree(
94
95
  type: prop.type,
95
96
  prop,
96
97
  index: 0,
97
- children: parseResolveTree(model, prop.type.entity, schema, f)
98
+ children: parseObjectTree(model, prop.type.entity, schema, f)
98
99
  })
99
100
  break
100
101
  case "list-lookup":
@@ -105,8 +106,8 @@ export function parseResolveTree(
105
106
  type: prop.type,
106
107
  prop,
107
108
  index: 0,
108
- args: parseEntityListArguments(model, prop.type.entity, f.args),
109
- children: parseResolveTree(model, prop.type.entity, schema, f)
109
+ args: parseSqlArguments(model, prop.type.entity, f.args),
110
+ children: parseObjectTree(model, prop.type.entity, schema, f)
110
111
  })
111
112
  break
112
113
  default:
@@ -118,8 +119,8 @@ export function parseResolveTree(
118
119
  }
119
120
 
120
121
 
121
- export function parseEntityListArguments(model: Model, entityName: string, gqlArgs: any): EntityListArguments {
122
- let args: EntityListArguments = {}
122
+ export function parseSqlArguments(model: Model, typeName: string, gqlArgs: any): SqlArguments {
123
+ let args: SqlArguments = {}
123
124
 
124
125
  let where = parseWhere(gqlArgs.where)
125
126
  if (where) {
@@ -127,7 +128,7 @@ export function parseEntityListArguments(model: Model, entityName: string, gqlAr
127
128
  }
128
129
 
129
130
  if (gqlArgs.orderBy) {
130
- args.orderBy = parseOrderBy(model, entityName, ensureArray(gqlArgs.orderBy))
131
+ args.orderBy = parseOrderBy(model, typeName, ensureArray(gqlArgs.orderBy))
131
132
  }
132
133
 
133
134
  if (gqlArgs.offset) {
@@ -142,3 +143,31 @@ export function parseEntityListArguments(model: Model, entityName: string, gqlAr
142
143
 
143
144
  return args
144
145
  }
146
+
147
+
148
+ export function parseQueryableTree(
149
+ model: Model,
150
+ queryableName: string,
151
+ schema: GraphQLSchema,
152
+ tree: ResolveTree
153
+ ): FieldsByEntity {
154
+ let fields: FieldsByEntity = {}
155
+ for (let entity of getQueryableEntities(model, queryableName)) {
156
+ fields[entity] = parseObjectTree(model, entity, schema, tree)
157
+ }
158
+ return fields
159
+ }
160
+
161
+
162
+ export function parseAnyTree(
163
+ model: Model,
164
+ typeName: string,
165
+ schema: GraphQLSchema,
166
+ tree: ResolveTree
167
+ ): AnyFields {
168
+ if (model[typeName].kind == 'interface') {
169
+ return parseQueryableTree(model, typeName, schema, tree)
170
+ } else {
171
+ return parseObjectTree(model, typeName, schema, tree)
172
+ }
173
+ }
package/src/server.ts CHANGED
@@ -1,21 +1,23 @@
1
- import type {Logger} from "@subsquid/logger"
2
- import {listen, ListeningServer} from "@subsquid/util-internal-http-server"
3
- import {PluginDefinition} from "apollo-server-core"
4
- import {ApolloServer} from "apollo-server-express"
5
- import express from "express"
6
- import fs from "fs"
7
- import {GraphQLSchema} from "graphql"
8
- import {useServer as useWsServer} from "graphql-ws/lib/use/ws"
9
- import http from "http"
10
- import path from "path"
11
- import type {Pool} from "pg"
12
- import {WebSocketServer} from "ws"
13
- import {Context} from "./context"
14
- import {PoolOpenreaderContext} from "./db"
15
- import type {Dialect} from "./dialect"
16
- import type {Model} from "./model"
17
- import {SchemaBuilder} from "./opencrud/schema"
18
- import {logGraphQLError} from "./util/error-handling"
1
+ import type {Logger} from '@subsquid/logger'
2
+ import {listen, ListeningServer} from '@subsquid/util-internal-http-server'
3
+ import {PluginDefinition} from 'apollo-server-core'
4
+ import {ApolloServer} from 'apollo-server-express'
5
+ import express from 'express'
6
+ import fs from 'fs'
7
+ import {ExecutionArgs, GraphQLSchema} from 'graphql'
8
+ import {useServer as useWsServer} from 'graphql-ws/lib/use/ws'
9
+ import http from 'http'
10
+ import path from 'path'
11
+ import type {Pool} from 'pg'
12
+ import {WebSocketServer} from 'ws'
13
+ import {Context, OpenreaderContext} from './context'
14
+ import {PoolOpenreaderContext} from './db'
15
+ import type {Dialect} from './dialect'
16
+ import type {Model} from './model'
17
+ import {SchemaBuilder} from './opencrud/schema'
18
+ import {logGraphQLError} from './util/error-handling'
19
+ import {executeWithLimit} from './util/execute'
20
+ import {ResponseSizeLimit} from './util/limit'
19
21
 
20
22
 
21
23
  export interface ServerOptions {
@@ -23,25 +25,46 @@ export interface ServerOptions {
23
25
  model: Model
24
26
  connection: Pool
25
27
  dialect?: Dialect
26
- subscriptions?: boolean
27
- subscriptionPollInterval?: number
28
- subscriptionConnection?: Pool
29
28
  graphiqlConsole?: boolean
30
29
  log?: Logger
31
30
  maxRequestSizeBytes?: number
31
+ maxRootFields?: number
32
+ maxResponseNodes?: number
33
+ subscriptions?: boolean
34
+ subscriptionPollInterval?: number
35
+ subscriptionConnection?: Pool
36
+ subscriptionMaxResponseNodes?: number
32
37
  }
33
38
 
34
39
 
35
40
  export async function serve(options: ServerOptions): Promise<ListeningServer> {
36
- let {connection, subscriptionConnection, subscriptionPollInterval} = options
41
+ let {connection, subscriptionConnection, subscriptionPollInterval, maxResponseNodes, subscriptionMaxResponseNodes} = options
37
42
  let dialect = options.dialect ?? 'postgres'
38
43
 
39
44
  let schema = new SchemaBuilder(options).build()
45
+
40
46
  let context = () => {
47
+ let openreader: OpenreaderContext = new PoolOpenreaderContext(
48
+ dialect,
49
+ connection,
50
+ subscriptionConnection,
51
+ subscriptionPollInterval
52
+ )
53
+
54
+ if (maxResponseNodes) {
55
+ openreader.responseSizeLimit = new ResponseSizeLimit(maxResponseNodes)
56
+ openreader.subscriptionResponseSizeLimit = new ResponseSizeLimit(maxResponseNodes)
57
+ }
58
+
59
+ if (subscriptionMaxResponseNodes) {
60
+ openreader.subscriptionResponseSizeLimit = new ResponseSizeLimit(subscriptionMaxResponseNodes)
61
+ }
62
+
41
63
  return {
42
- openreader: new PoolOpenreaderContext(dialect, connection, subscriptionConnection, subscriptionPollInterval)
64
+ openreader
43
65
  }
44
66
  }
67
+
45
68
  let disposals: Dispose[] = []
46
69
 
47
70
  return addServerCleanup(disposals, runApollo({
@@ -52,7 +75,8 @@ export async function serve(options: ServerOptions): Promise<ListeningServer> {
52
75
  subscriptions: options.subscriptions,
53
76
  log: options.log,
54
77
  graphiqlConsole: options.graphiqlConsole,
55
- maxRequestSizeBytes: options.maxRequestSizeBytes
78
+ maxRequestSizeBytes: options.maxRequestSizeBytes,
79
+ maxRootFields: options.maxRootFields
56
80
  }), options.log)
57
81
  }
58
82
 
@@ -70,15 +94,21 @@ export interface ApolloOptions {
70
94
  graphiqlConsole?: boolean
71
95
  log?: Logger
72
96
  maxRequestSizeBytes?: number
97
+ maxRootFields?: number
73
98
  }
74
99
 
75
100
 
76
101
  export async function runApollo(options: ApolloOptions): Promise<ListeningServer> {
77
- let {disposals, context, schema, log} = options
102
+ const {disposals, context, schema, log, maxRootFields} = options
103
+
78
104
  let maxRequestSizeBytes = options.maxRequestSizeBytes ?? 256 * 1024
79
105
  let app = express()
80
106
  let server = http.createServer(app)
81
107
 
108
+ const execute = maxRootFields
109
+ ? (args: ExecutionArgs) => executeWithLimit(maxRootFields, args)
110
+ : undefined
111
+
82
112
  if (options.subscriptions) {
83
113
  let wsServer = new WebSocketServer({
84
114
  server,
@@ -89,6 +119,7 @@ export async function runApollo(options: ApolloOptions): Promise<ListeningServer
89
119
  {
90
120
  schema,
91
121
  context,
122
+ execute,
92
123
  onError(ctx, message, errors) {
93
124
  if (log) {
94
125
  // FIXME: we don't want to log client errors
@@ -111,6 +142,16 @@ export async function runApollo(options: ApolloOptions): Promise<ListeningServer
111
142
  schema,
112
143
  context,
113
144
  stopOnTerminationSignals: false,
145
+ executor: execute && (async req => {
146
+ return execute({
147
+ schema,
148
+ document: req.document,
149
+ rootValue: {},
150
+ contextValue: req.context,
151
+ variableValues: req.request.variables,
152
+ operationName: req.operationName
153
+ })
154
+ }),
114
155
  plugins: [
115
156
  ...options.plugins || [],
116
157
  {
@@ -136,7 +177,6 @@ export async function runApollo(options: ApolloOptions): Promise<ListeningServer
136
177
  setupGraphiqlConsole(app)
137
178
  }
138
179
 
139
-
140
180
  await apollo.start()
141
181
  disposals.push(() => apollo.stop())
142
182
 
package/src/sql/cursor.ts CHANGED
@@ -154,7 +154,7 @@ export class EntityCursor implements Cursor {
154
154
  case "fk":
155
155
  return new EntityCursor(
156
156
  this.ctx,
157
- prop.type.foreignEntity,
157
+ prop.type.entity,
158
158
  {on: 'id', rhs: this.native(field)}
159
159
  )
160
160
  case "lookup":
@@ -281,7 +281,7 @@ export class ObjectCursor implements Cursor {
281
281
  case "fk":
282
282
  return new EntityCursor(
283
283
  this.ctx,
284
- prop.type.foreignEntity,
284
+ prop.type.entity,
285
285
  {on: 'id', rhs: this.string(field)}
286
286
  )
287
287
  default:
@@ -1,5 +1,5 @@
1
1
  import {unexpectedCase} from "@subsquid/util-internal"
2
- import {FieldRequest} from "../ir/fields"
2
+ import {FieldRequest, FieldsByEntity} from '../ir/fields'
3
3
 
4
4
 
5
5
  export function mapRows(rows: any[][], fields: FieldRequest[]): any[] {
@@ -64,3 +64,20 @@ export function mapRow(row: any[], fields: FieldRequest[], ifType?: string): any
64
64
  }
65
65
  return rec
66
66
  }
67
+
68
+
69
+ export function mapQueryableRows(rows: any[][], fields: FieldsByEntity): any[] {
70
+ let result = new Array(rows.length)
71
+ for (let i = 0; i < rows.length; i++) {
72
+ result[i] = mapQueryableRow(rows[i], fields)
73
+ }
74
+ return result
75
+ }
76
+
77
+
78
+ export function mapQueryableRow(row: any[], fields: FieldsByEntity): any {
79
+ let entity = row[0]
80
+ let rec = mapRow(row[1], fields[entity])
81
+ rec._isTypeOf = entity
82
+ return rec
83
+ }
@@ -1,14 +1,15 @@
1
- import {unexpectedCase} from "@subsquid/util-internal"
2
- import assert from "assert"
3
- import {Dialect} from "../dialect"
4
- import {EntityListArguments, OrderBy, Where} from "../ir/args"
5
- import {FieldRequest} from "../ir/fields"
6
- import {Model} from "../model"
7
- import {Cursor, EntityCursor} from "./cursor"
8
- import {AliasSet, ColumnSet, escapeIdentifier, JoinSet, printClause} from "./util"
1
+ import {unexpectedCase} from '@subsquid/util-internal'
2
+ import assert from 'assert'
3
+ import {Dialect} from '../dialect'
4
+ import {OrderBy, SortOrder, SqlArguments, Where} from '../ir/args'
5
+ import {FieldRequest, FieldsByEntity} from '../ir/fields'
6
+ import {Model} from '../model'
7
+ import {getQueryableEntities} from '../model.tools'
8
+ import {Cursor, EntityCursor} from './cursor'
9
+ import {AliasSet, ColumnSet, escapeIdentifier, JoinSet, printClause} from './util'
9
10
 
10
11
 
11
- export class EntityListQueryPrinter {
12
+ export class EntitySqlPrinter {
12
13
  private aliases: AliasSet
13
14
  private join: JoinSet
14
15
  private root: EntityCursor
@@ -19,9 +20,9 @@ export class EntityListQueryPrinter {
19
20
  constructor(
20
21
  private model: Model,
21
22
  private dialect: Dialect,
22
- private entityName: string,
23
+ public readonly entityName: string,
23
24
  private params: unknown[],
24
- private args: EntityListArguments = {},
25
+ private args: SqlArguments = {},
25
26
  fields?: FieldRequest[],
26
27
  aliases?: AliasSet
27
28
  ) {
@@ -43,12 +44,14 @@ export class EntityListQueryPrinter {
43
44
  this.populateWhere(this.root, args.where, this.where)
44
45
  }
45
46
  if (args.orderBy) {
46
- this.populateOrderBy(this.root, args.orderBy)
47
+ this.traverseOrderBy(args.orderBy, (field, cursor, order) => {
48
+ this.orderBy.push(cursor.native(field) + ' ' + order)
49
+ })
47
50
  }
48
51
  }
49
52
 
50
- private sub(entityName: string, args?: EntityListArguments, fields?: FieldRequest[]): EntityListQueryPrinter {
51
- return new EntityListQueryPrinter(this.model, this.dialect, entityName, this.params, args, fields, this.aliases)
53
+ private sub(entityName: string, args?: SqlArguments, fields?: FieldRequest[]): EntitySqlPrinter {
54
+ return new EntitySqlPrinter(this.model, this.dialect, entityName, this.params, args, fields, this.aliases)
52
55
  }
53
56
 
54
57
  private populateColumns(cursor: Cursor, fields: FieldRequest[]): void {
@@ -128,7 +131,9 @@ export class EntityListQueryPrinter {
128
131
  break
129
132
  }
130
133
  case "isNull": {
131
- let f = cursor.ref(where.field)
134
+ let f = cursor.prop(where.field).type.kind == 'lookup'
135
+ ? cursor.child(where.field).ref('id')
136
+ : cursor.ref(where.field)
132
137
  if (where.yes) {
133
138
  exps.push(`${f} IS NULL`)
134
139
  } else {
@@ -261,13 +266,17 @@ export class EntityListQueryPrinter {
261
266
  return printClause("AND", exps)
262
267
  }
263
268
 
264
- private populateOrderBy(cursor: Cursor, orderBy: OrderBy): void {
269
+ traverseOrderBy(orderBy: OrderBy, cb: (field: string, cursor: Cursor, order: SortOrder) => void) {
270
+ this.visitOrderBy(this.root, orderBy, cb)
271
+ }
272
+
273
+ private visitOrderBy(cursor: Cursor, orderBy: OrderBy, cb: (field: string, cursor: Cursor, order: SortOrder) => void) {
265
274
  for (let field in orderBy) {
266
275
  let spec = orderBy[field]
267
276
  if (typeof spec == "string") {
268
- this.orderBy.push(`${cursor.native(field)} ${spec}`)
277
+ cb(field, cursor, spec)
269
278
  } else {
270
- this.populateOrderBy(cursor.child(field), spec)
279
+ this.visitOrderBy(cursor.child(field), spec, cb)
271
280
  }
272
281
  }
273
282
  }
@@ -280,14 +289,18 @@ export class EntityListQueryPrinter {
280
289
  return escapeIdentifier(this.dialect, name)
281
290
  }
282
291
 
283
- addWhereDerivedFrom(field: string, parentIdExp: string): this {
292
+ private addWhereDerivedFrom(field: string, parentIdExp: string): this {
284
293
  this.where.push(`${this.root.native(field)} = ${parentIdExp}`)
285
294
  return this
286
295
  }
287
296
 
297
+ hasColumns(): boolean {
298
+ return this.columns.size() > 0
299
+ }
300
+
288
301
  printColumnList(options?: {withAliases?: boolean}): string {
302
+ assert(this.hasColumns())
289
303
  let names = this.columns.names()
290
- assert(names.length > 0)
291
304
  if (options?.withAliases) {
292
305
  names = names.map((name, idx) => `${name} AS _c${idx}`)
293
306
  }
@@ -322,7 +335,110 @@ export class EntityListQueryPrinter {
322
335
  return `SELECT ${this.printColumnList({withAliases: true})} ${this.printFrom()}`
323
336
  }
324
337
 
325
- printAsJsonRows(): string {
338
+ printAsCount(): string {
339
+ if (this.args.offset || this.args.limit) {
340
+ return `SELECT count(*) FROM (SELECT true ${this.printFrom()}) AS rows`
341
+ } else {
342
+ return `SELECT count(*) ${this.printFrom()}`
343
+ }
344
+ }
345
+
346
+ private printAsJsonRows(): string {
326
347
  return `SELECT ${this.printColumnListAsJsonArray()} AS row ${this.printFrom()}`
327
348
  }
328
349
  }
350
+
351
+
352
+ export class QueryableSqlPrinter {
353
+ private printers: EntitySqlPrinter[] = []
354
+ private orders: SortOrder[] = []
355
+ private orderColumns: string[][] = []
356
+
357
+ constructor(
358
+ private model: Model,
359
+ private dialect: Dialect,
360
+ private queryableName: string,
361
+ private params: unknown[],
362
+ private args: SqlArguments = {},
363
+ fields?: FieldsByEntity
364
+ ) {
365
+ for (let entityName of getQueryableEntities(this.model, this.queryableName)) {
366
+ let entityFields = fields?.[entityName]
367
+
368
+ let printer = new EntitySqlPrinter(
369
+ model,
370
+ dialect,
371
+ entityName,
372
+ this.params,
373
+ {where: args.where},
374
+ entityFields
375
+ )
376
+
377
+ if (this.args.orderBy) {
378
+ let cols: string[] = []
379
+ this.orders.length = 0
380
+ printer.traverseOrderBy(this.args.orderBy, (field, cursor, order) => {
381
+ let col = field == '_type' ? `'${entityName}'` : cursor.native(field)
382
+ this.orders.push(order)
383
+ cols.push(`${col} AS o${this.orders.length}`)
384
+ })
385
+ this.orderColumns.push(cols)
386
+ }
387
+
388
+ this.printers.push(printer)
389
+ }
390
+ }
391
+
392
+ print(): string {
393
+ let from = this.printers.map((printer, idx) => {
394
+ let cols: string[] = []
395
+ cols.push(`'${printer.entityName}' AS e`)
396
+ if (printer.hasColumns()) {
397
+ cols.push(printer.printColumnListAsJsonArray() + ' AS d')
398
+ } else {
399
+ cols.push('null AS d')
400
+ }
401
+ cols.push(...this.orderColumns[idx])
402
+ return `SELECT ${cols.join(', ')} ${printer.printFrom()}`
403
+ }).join('\nUNION ALL\n')
404
+
405
+ let args = this.printArgs()
406
+ if (args) {
407
+ return `SELECT e, d FROM (\n${from}\n) AS rows` + args
408
+ } else {
409
+ return from
410
+ }
411
+ }
412
+
413
+ printAsCount(): string {
414
+ let union = this.orders.length
415
+ ? this.printers.map((printer, idx) => {
416
+ return `SELECT ${this.orderColumns[idx].join(', ')} ${printer.printFrom()}`
417
+ })
418
+ : this.printers.map(printer => {
419
+ return `SELECT true ${printer.printFrom()}`
420
+ })
421
+
422
+ let from = union.join('\nUNION ALL\n')
423
+ let args = this.printArgs()
424
+ if (args) {
425
+ from = `SELECT true FROM (\n${from}\n) AS src` + args
426
+ }
427
+
428
+ return `SELECT count(*) FROM (\n${from}\n) AS rows`
429
+ }
430
+
431
+ private printArgs(): string {
432
+ let sql = ''
433
+ if (this.orders.length) {
434
+ sql += '\nORDER BY ' + this.orders.map((o, idx) => `o${idx + 1} ${o}`).join(', ')
435
+ }
436
+ if (this.args.offset) {
437
+ sql += `\nOFFSET ${this.args.offset}`
438
+ }
439
+ if (this.args.limit) {
440
+ sql += `\nLIMIT ${this.args.limit}`
441
+ }
442
+ return sql
443
+ }
444
+ }