@sigitex/outlaw 1.2.0 → 2.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.
@@ -0,0 +1,272 @@
1
+ import type { BinaryOperator, UnaryOperator } from "../api/api.types"
2
+ import type { ColumnRef } from "../schemaBuilder/ColumnRef"
3
+ import type { InferColumn } from "../schemaBuilder/schemaBuilder.types"
4
+ import type { QuerySource } from "./QuerySource"
5
+ import type { JoinType } from "./queryBuilders.types"
6
+
7
+ export type QueryScope = {
8
+ sources: readonly QueryScope.State[]
9
+ output: QueryScope.Entry
10
+ }
11
+
12
+ export namespace QueryScope {
13
+ export type Column = { value: unknown; mapped: boolean }
14
+ export type Columns = Record<string, Column>
15
+ export type State<
16
+ Name extends string = string,
17
+ Fields extends Columns = Columns,
18
+ > = {
19
+ name: Name
20
+ columns: Fields
21
+ }
22
+ export type Entry = {
23
+ name: string
24
+ value: unknown
25
+ mapped: boolean
26
+ origin: string
27
+ }
28
+ export type SchemaColumns<Fields> = {
29
+ [Key in keyof Fields & string]: {
30
+ value: InferColumn<Fields[Key]>
31
+ mapped: Fields[Key] extends { readonly $defined?: infer Defined }
32
+ ? "map" extends Defined
33
+ ? true
34
+ : false
35
+ : false
36
+ }
37
+ }
38
+ export type Of<Target> = Target extends {
39
+ readonly $type: infer Shape extends State
40
+ }
41
+ ? Shape
42
+ : never
43
+ export type Initial<Shape extends State> = {
44
+ sources: [Shape]
45
+ output: Entries<Shape>
46
+ }
47
+ export type Entries<Shape extends State> = {
48
+ [Key in keyof Shape["columns"] & string]: {
49
+ value: Shape["columns"][Key]["value"]
50
+ mapped: Shape["columns"][Key]["mapped"]
51
+ name: Key
52
+ origin: `${Shape["name"]}.${Key}`
53
+ }
54
+ }[keyof Shape["columns"] & string]
55
+
56
+ export type Identifier<Scope extends QueryScope> =
57
+ | Scope["output"]["name"]
58
+ | SourceIdentifiers<Scope["sources"][number]>
59
+ export type Projection<Scope extends QueryScope> =
60
+ | Identifier<Scope>
61
+ | "*"
62
+ | Wildcards<Scope["sources"][number]>
63
+ export type Resolve<Scope extends QueryScope, Input> = Input extends {
64
+ table: infer Table
65
+ column: infer Key
66
+ }
67
+ ? Qualified<Scope, Table, Key>
68
+ : Input extends Scope["output"]["name"]
69
+ ? Extract<Scope["output"], { name: Input }>
70
+ : Input extends `${infer Table}.${infer Key}`
71
+ ? Qualified<Scope, Table, Key>
72
+ : never
73
+
74
+ export type Project<
75
+ Scope extends QueryScope,
76
+ Inputs extends readonly unknown[],
77
+ > = Collapse<
78
+ Inputs extends readonly []
79
+ ? Scope["output"]
80
+ : ProjectEntry<Scope, Inputs[number]>
81
+ >
82
+ export type Values<Fields extends Columns> = {
83
+ [Key in keyof Fields]: Fields[Key]["value"]
84
+ }
85
+
86
+ export type Join<
87
+ Scope extends QueryScope,
88
+ Right extends State,
89
+ Kind extends JoinType,
90
+ > = {
91
+ sources: [
92
+ ...ExtendSources<Scope["sources"], Kind>,
93
+ ExtendRight<Right, Kind>,
94
+ ]
95
+ output:
96
+ | ExtendLeft<Scope["output"], Kind>
97
+ | Entries<ExtendRight<Right, Kind>>
98
+ }
99
+ export type Using<
100
+ Scope extends QueryScope,
101
+ Right extends State,
102
+ Kind extends JoinType,
103
+ Keys extends string,
104
+ > = {
105
+ sources: Join<Scope, Right, Kind>["sources"]
106
+ output:
107
+ | Exclude<Join<Scope, Right, Kind>["output"], { name: Keys }>
108
+ | Common<Scope, Right, Kind, Keys>
109
+ }
110
+
111
+ export type Composition<
112
+ Scope extends QueryScope,
113
+ Executable extends boolean = false,
114
+ > = {
115
+ select<const Inputs extends readonly Projection<Scope>[]>(
116
+ ...columns: Inputs
117
+ ): Selection<Project<Scope, Inputs>, Scope, Executable>
118
+ join<Target extends QuerySource.Target>(
119
+ target: Target,
120
+ ): Joined<Scope, Of<Target>, "join", Executable>
121
+ leftJoin<Target extends QuerySource.Target>(
122
+ target: Target,
123
+ ): Joined<Scope, Of<Target>, "left join", Executable>
124
+ rightJoin<Target extends QuerySource.Target>(
125
+ target: Target,
126
+ ): Joined<Scope, Of<Target>, "right join", Executable>
127
+ fullJoin<Target extends QuerySource.Target>(
128
+ target: Target,
129
+ ): Joined<Scope, Of<Target>, "full join", Executable>
130
+ crossJoin<Target extends QuerySource.Target>(
131
+ target: Target,
132
+ ): Joined<Scope, Of<Target>, "cross join", Executable>
133
+ }
134
+ export type Joined<
135
+ Left extends QueryScope,
136
+ Right extends State,
137
+ Kind extends JoinType,
138
+ Executable extends boolean,
139
+ Constraint extends "none" | "on" = "none",
140
+ > = Composition<Join<Left, Right, Kind>, Executable> & {
141
+ on(
142
+ left: Identifier<Join<Left, Right, Kind>>,
143
+ operator: BinaryOperator,
144
+ right: Identifier<Join<Left, Right, Kind>>,
145
+ ): Joined<Left, Right, Kind, Executable, "on">
146
+ } & (Constraint extends "none"
147
+ ? {
148
+ using<
149
+ const Keys extends readonly [
150
+ Left["output"]["name"] & keyof Right["columns"] & string,
151
+ ...(Left["output"]["name"] & keyof Right["columns"] & string)[],
152
+ ],
153
+ >(
154
+ ...columns: Keys
155
+ ): Composition<Using<Left, Right, Kind, Keys[number]>, Executable>
156
+ }
157
+ : {})
158
+
159
+ export type Selection<
160
+ Fields extends Columns,
161
+ Scope extends QueryScope,
162
+ Executable extends boolean = false,
163
+ > = {
164
+ readonly $type: State<"", Fields>
165
+ as<Name extends string>(name: Name): QuerySource<Name, Fields>
166
+ where<const Input extends Identifier<Scope>>(
167
+ column: Input,
168
+ value: Value<Resolve<Scope, NoInfer<Input>>> | UnaryOperator,
169
+ ): Selection<Fields, Scope, Executable>
170
+ where<const Input extends Identifier<Scope>>(
171
+ column: Input,
172
+ operator: BinaryOperator,
173
+ value: Value<Resolve<Scope, NoInfer<Input>>>,
174
+ ): Selection<Fields, Scope, Executable>
175
+ orderBy(
176
+ sorts: readonly (readonly [
177
+ Identifier<Scope> | (keyof Fields & string),
178
+ "asc" | "desc",
179
+ ])[],
180
+ ): Selection<Fields, Scope, Executable>
181
+ limit(count: number): Selection<Fields, Scope, Executable>
182
+ offset(count: number): Selection<Fields, Scope, Executable>
183
+ } & (Executable extends true
184
+ ? {
185
+ fetch(): Promise<Values<Fields>[]>
186
+ first(): Promise<Values<Fields>>
187
+ }
188
+ : {})
189
+
190
+ type SourceIdentifiers<Shape extends State> = Shape extends State
191
+ ? {
192
+ [Key in keyof Shape["columns"] & string]:
193
+ | ColumnRef<Shape["name"], Key>
194
+ | (Shape["name"] extends "" ? never : `${Shape["name"]}.${Key}`)
195
+ }[keyof Shape["columns"] & string]
196
+ : never
197
+ type Wildcards<Shape extends State> = Shape extends State
198
+ ? ColumnRef.Wildcard<Shape["name"]>
199
+ : never
200
+ type Qualified<Scope extends QueryScope, Name, Key> =
201
+ Entries<
202
+ Extract<Scope["sources"][number], { name: Name }>
203
+ > extends infer Fields
204
+ ? Extract<Fields, { name: Key }>
205
+ : never
206
+ type ProjectEntry<Scope extends QueryScope, Input> = Input extends "*"
207
+ ? Scope["output"]
208
+ : Input extends { table: infer Table; wildcard: true }
209
+ ? Entries<Extract<Scope["sources"][number], { name: Table }>>
210
+ : Input extends { alias: infer Alias extends string }
211
+ ? Rename<Resolve<Scope, Input>, Alias>
212
+ : Resolve<Scope, Input>
213
+ type Rename<Fields extends Entry, Name extends string> = Fields extends Entry
214
+ ? Omit<Fields, "name"> & { name: Name }
215
+ : never
216
+ type IsUnion<Value, Whole = Value> = Value extends Whole
217
+ ? [Whole] extends [Value]
218
+ ? false
219
+ : true
220
+ : never
221
+ type Value<Fields extends Entry> =
222
+ true extends IsUnion<Fields["origin"]>
223
+ ? true extends Fields["mapped"]
224
+ ? unknown
225
+ : Fields["value"]
226
+ : Fields["value"]
227
+ type Collapse<Fields extends Entry> = {
228
+ [Name in Fields["name"]]: {
229
+ value: Value<Extract<Fields, { name: Name }>>
230
+ mapped: true extends IsUnion<Extract<Fields, { name: Name }>["origin"]>
231
+ ? false
232
+ : Extract<Fields, { name: Name }>["mapped"]
233
+ }
234
+ }
235
+ type Nullable<Fields extends Columns> = {
236
+ [Key in keyof Fields]: {
237
+ value: Fields[Key]["value"] | null
238
+ mapped: Fields[Key]["mapped"]
239
+ }
240
+ }
241
+ type ExtendSources<
242
+ Sources extends readonly State[],
243
+ Kind extends JoinType,
244
+ > = {
245
+ [Index in keyof Sources]: Kind extends "right join" | "full join"
246
+ ? State<Sources[Index]["name"], Nullable<Sources[Index]["columns"]>>
247
+ : Sources[Index]
248
+ }
249
+ type ExtendRight<Right extends State, Kind extends JoinType> = Kind extends
250
+ | "left join"
251
+ | "full join"
252
+ ? State<Right["name"], Nullable<Right["columns"]>>
253
+ : Right
254
+ type ExtendLeft<
255
+ Fields extends Entry,
256
+ Kind extends JoinType,
257
+ > = Fields extends Entry
258
+ ? Kind extends "right join" | "full join"
259
+ ? Omit<Fields, "value"> & { value: Fields["value"] | null }
260
+ : Fields
261
+ : never
262
+ type Common<
263
+ Scope extends QueryScope,
264
+ Right extends State,
265
+ Kind extends JoinType,
266
+ Keys extends string,
267
+ > = Kind extends "right join"
268
+ ? Extract<Entries<Right>, { name: Keys }>
269
+ : Kind extends "full join"
270
+ ? Extract<Scope["output"] | Entries<Right>, { name: Keys }>
271
+ : Extract<Scope["output"], { name: Keys }>
272
+ }
@@ -0,0 +1,231 @@
1
+ import type { Connection } from "../api/api.types"
2
+ import { ColumnRef } from "../schemaBuilder/ColumnRef"
3
+ import type { ColumnData, TableData } from "../schemaBuilder/metadata"
4
+ import type { QueryScope } from "./QueryScope"
5
+ import type { SelectQuery, ColumnIdentifier } from "./queryBuilders.types"
6
+ import { QuerySourceBuilder } from "./QuerySourceBuilder"
7
+ import { Projection } from "./Projection"
8
+
9
+ export type QuerySource<
10
+ Name extends string,
11
+ Columns extends QueryScope.Columns,
12
+ > = {
13
+ readonly $source: QuerySource.Data
14
+ readonly $type: QueryScope.State<Name, Columns>
15
+ readonly all: ColumnRef.Wildcard<Name>
16
+ as<Alias extends string>(alias: Alias): QuerySource<Alias, Columns>
17
+ } & {
18
+ readonly [Key in keyof Columns & string]: ColumnRef<Name, Key>
19
+ } & QueryScope.Composition<QueryScope.Initial<QueryScope.State<Name, Columns>>>
20
+
21
+ export namespace QuerySource {
22
+ export type Data = (
23
+ | { kind: "table"; name: string }
24
+ | { kind: "subquery"; query: SelectQuery }
25
+ ) & { alias?: string; tableData: TableData }
26
+
27
+ export type Target = { readonly $type: QueryScope.State }
28
+ export type Input =
29
+ | { readonly $source: Data }
30
+ | { readonly query: SelectQuery }
31
+ export type Scope = {
32
+ sources: Data[]
33
+ output: ColumnData[]
34
+ coalesced: Set<string>
35
+ }
36
+
37
+ export function create(
38
+ data: Data,
39
+ connection?: Connection,
40
+ ): QuerySource<string, QueryScope.Columns> {
41
+ const source = snapshotSource(data)
42
+ const name = qualifier(source)
43
+ const result = {
44
+ $source: source,
45
+ all: { table: name, wildcard: true as const },
46
+ as(alias: string) {
47
+ return create({ ...source, alias }, connection)
48
+ },
49
+ select(...columns: Projection.Input[]) {
50
+ return new QuerySourceBuilder(source, connection).select(...columns)
51
+ },
52
+ join(target: Input) {
53
+ return new QuerySourceBuilder(source, connection).join(target)
54
+ },
55
+ leftJoin(target: Input) {
56
+ return new QuerySourceBuilder(source, connection).leftJoin(target)
57
+ },
58
+ rightJoin(target: Input) {
59
+ return new QuerySourceBuilder(source, connection).rightJoin(target)
60
+ },
61
+ fullJoin(target: Input) {
62
+ return new QuerySourceBuilder(source, connection).fullJoin(target)
63
+ },
64
+ crossJoin(target: Input) {
65
+ return new QuerySourceBuilder(source, connection).crossJoin(target)
66
+ },
67
+ }
68
+ for (const column of source.tableData.columns) {
69
+ Object.defineProperty(result, column.name, {
70
+ value: ColumnRef.create(name, column.name),
71
+ enumerable: true,
72
+ })
73
+ }
74
+ return result as unknown as QuerySource<string, QueryScope.Columns>
75
+ }
76
+
77
+ export function target(input: Input): Data {
78
+ if ("$source" in input) {
79
+ return snapshotSource(input.$source)
80
+ }
81
+ const query = snapshot(input.query)
82
+ return {
83
+ kind: "subquery",
84
+ query,
85
+ tableData: {
86
+ name: "",
87
+ columns: Projection.columns(query),
88
+ constraints: [],
89
+ },
90
+ }
91
+ }
92
+
93
+ export function qualifier(source: Data): string {
94
+ return source.alias ?? (source.kind === "table" ? source.name : "")
95
+ }
96
+
97
+ export function snapshot(query: SelectQuery): SelectQuery {
98
+ return {
99
+ ...query,
100
+ source: snapshotSource(query.source),
101
+ selected: query.selected.map((projection) =>
102
+ projection === "*" ? projection : { ...projection },
103
+ ),
104
+ conditions: query.conditions?.map((condition) => ({ ...condition })),
105
+ orderBy: query.orderBy?.map((sort) => ({ ...sort })),
106
+ joins: query.joins?.map((clause) => ({
107
+ ...clause,
108
+ target: snapshotSource(clause.target),
109
+ on: clause.on.map((condition) => ({ ...condition })),
110
+ using: clause.using ? [...clause.using] : undefined,
111
+ })),
112
+ }
113
+ }
114
+
115
+ export function scope(query: SelectQuery): Scope {
116
+ let sources = [query.source]
117
+ let output = [...query.source.tableData.columns]
118
+ const coalesced = new Set<string>()
119
+ for (const clause of query.joins ?? []) {
120
+ const right = clause.target
121
+ const leftOutput = output
122
+ const extendLeft =
123
+ clause.type === "right join" || clause.type === "full join"
124
+ const extendRight =
125
+ clause.type === "left join" || clause.type === "full join"
126
+ if (extendLeft) {
127
+ sources = sources.map(nullableSource)
128
+ output = output.map(nullableColumn)
129
+ }
130
+ const incoming = extendRight ? nullableSource(right) : right
131
+ sources.push(incoming)
132
+ if (clause.using?.length) {
133
+ const common = new Set(clause.using)
134
+ for (const name of common) {
135
+ if (clause.type === "full join") {
136
+ coalesced.add(name)
137
+ } else if (clause.type === "right join") {
138
+ coalesced.delete(name)
139
+ }
140
+ }
141
+ output = output.map((column) => {
142
+ if (!common.has(column.name)) {
143
+ return column
144
+ }
145
+ const left = leftOutput.find(
146
+ (candidate) => candidate.name === column.name,
147
+ )!
148
+ const rightColumn = right.tableData.columns.find(
149
+ (candidate) => candidate.name === column.name,
150
+ )!
151
+ if (clause.type === "right join") {
152
+ return rightColumn
153
+ }
154
+ if (clause.type === "full join") {
155
+ return Projection.merge([left, rightColumn])
156
+ }
157
+ return left
158
+ })
159
+ output.push(
160
+ ...incoming.tableData.columns.filter(
161
+ (column) => !common.has(column.name),
162
+ ),
163
+ )
164
+ } else {
165
+ output.push(...incoming.tableData.columns)
166
+ }
167
+ }
168
+ return { sources, output, coalesced }
169
+ }
170
+
171
+ export function identifier(
172
+ scope: Scope,
173
+ input: string | ColumnRef,
174
+ ): ColumnIdentifier {
175
+ if (typeof input !== "string") {
176
+ return { table: input.table || undefined, column: input.column }
177
+ }
178
+ if (scope.output.some((column) => column.name === input)) {
179
+ return { column: input }
180
+ }
181
+ for (const source of scope.sources) {
182
+ const name = qualifier(source)
183
+ if (name && input.startsWith(`${name}.`)) {
184
+ return { table: name, column: input.slice(name.length + 1) }
185
+ }
186
+ }
187
+ return { column: input }
188
+ }
189
+
190
+ export function resolve(
191
+ scope: Scope,
192
+ identifier: ColumnIdentifier,
193
+ ): ColumnData[] {
194
+ if (identifier.table === undefined) {
195
+ return scope.output.filter((column) => column.name === identifier.column)
196
+ }
197
+ return scope.sources
198
+ .filter((source) => qualifier(source) === identifier.table)
199
+ .flatMap((source) =>
200
+ source.tableData.columns.filter(
201
+ (column) => column.name === identifier.column,
202
+ ),
203
+ )
204
+ }
205
+
206
+ function snapshotSource(source: Data): Data {
207
+ return {
208
+ ...source,
209
+ ...(source.kind === "subquery" ? { query: snapshot(source.query) } : {}),
210
+ tableData: {
211
+ ...source.tableData,
212
+ columns: [...source.tableData.columns],
213
+ constraints: [...source.tableData.constraints],
214
+ },
215
+ }
216
+ }
217
+
218
+ function nullableColumn(column: ColumnData): ColumnData {
219
+ return { ...column, notNull: false, primaryKey: undefined }
220
+ }
221
+
222
+ function nullableSource(source: Data): Data {
223
+ return {
224
+ ...source,
225
+ tableData: {
226
+ ...source.tableData,
227
+ columns: source.tableData.columns.map(nullableColumn),
228
+ },
229
+ }
230
+ }
231
+ }
@@ -0,0 +1,76 @@
1
+ import type { BinaryOperator, Connection } from "../api/api.types"
2
+ import type { ColumnRef } from "../schemaBuilder/ColumnRef"
3
+ import { Projection } from "./Projection"
4
+ import { QuerySource } from "./QuerySource"
5
+ import { SelectBuilder } from "./SelectBuilder"
6
+ import { SelectQueryBuilder } from "./SelectQueryBuilder"
7
+ import type { JoinType, SelectQuery } from "./queryBuilders.types"
8
+
9
+ export class QuerySourceBuilder {
10
+ private readonly query: SelectQuery
11
+ private readonly connection?: Connection
12
+
13
+ constructor(
14
+ source: QuerySource.Data,
15
+ connection?: Connection,
16
+ query?: SelectQuery,
17
+ ) {
18
+ this.query = query ?? { source, selected: ["*"] }
19
+ this.connection = connection
20
+ }
21
+
22
+ select(...columns: Projection.Input[]) {
23
+ const query = QuerySource.snapshot(this.query)
24
+ const scope = QuerySource.scope(query)
25
+ query.selected = columns.length
26
+ ? columns.map((column) => Projection.create(scope, column))
27
+ : ["*"]
28
+ return this.connection
29
+ ? new SelectBuilder(this.connection, query)
30
+ : new SelectQueryBuilder(query)
31
+ }
32
+
33
+ join(target: QuerySource.Input) {
34
+ return this.addJoin("join", target)
35
+ }
36
+ leftJoin(target: QuerySource.Input) {
37
+ return this.addJoin("left join", target)
38
+ }
39
+ rightJoin(target: QuerySource.Input) {
40
+ return this.addJoin("right join", target)
41
+ }
42
+ fullJoin(target: QuerySource.Input) {
43
+ return this.addJoin("full join", target)
44
+ }
45
+ crossJoin(target: QuerySource.Input) {
46
+ return this.addJoin("cross join", target)
47
+ }
48
+
49
+ on(
50
+ left: string | ColumnRef,
51
+ operator: BinaryOperator,
52
+ right: string | ColumnRef,
53
+ ) {
54
+ const query = QuerySource.snapshot(this.query)
55
+ const scope = QuerySource.scope(query)
56
+ query.joins![query.joins!.length - 1].on.push({
57
+ left: QuerySource.identifier(scope, left),
58
+ operator,
59
+ right: QuerySource.identifier(scope, right),
60
+ })
61
+ return new QuerySourceBuilder(query.source, this.connection, query)
62
+ }
63
+
64
+ using(...columns: string[]) {
65
+ const query = QuerySource.snapshot(this.query)
66
+ query.joins![query.joins!.length - 1].using = columns
67
+ return new QuerySourceBuilder(query.source, this.connection, query)
68
+ }
69
+
70
+ private addJoin(type: JoinType, input: QuerySource.Input) {
71
+ const query = QuerySource.snapshot(this.query)
72
+ query.joins ??= []
73
+ query.joins.push({ type, target: QuerySource.target(input), on: [] })
74
+ return new QuerySourceBuilder(query.source, this.connection, query)
75
+ }
76
+ }
@@ -1,37 +1,31 @@
1
- // oxlint-disable typescript/no-explicit-any
2
- import type { Connection, Select } from "../api"
3
- import type { TableData } from "../schemaBuilder"
4
- import { generateSelect } from "../queryGenerator"
1
+ import type { Connection } from "../api/api.types"
2
+ import { generateSelect } from "../queryGenerator/generateSelect"
5
3
  import { Mappings } from "./Mappings"
4
+ import { Projection } from "./Projection"
6
5
  import { SelectQueryBuilder } from "./SelectQueryBuilder"
6
+ import type { SelectQuery } from "./queryBuilders.types"
7
7
 
8
- /** Constructs declarative `SELECT` statements with connection-based execution. */
9
- export class SelectBuilder extends SelectQueryBuilder implements Select<any, any> {
8
+ export class SelectBuilder extends SelectQueryBuilder {
10
9
  readonly connection: Connection
11
10
 
12
- constructor(
13
- connection: Connection,
14
- table: TableData,
15
- columns: "*" | string[],
16
- ) {
17
- super(table, columns)
11
+ constructor(connection: Connection, query: SelectQuery) {
12
+ super(query)
18
13
  this.connection = connection
19
14
  }
20
15
 
21
- async fetch(): Promise<any[]> {
16
+ async fetch() {
22
17
  const rows = await this.connection.query(generateSelect(this.query))
23
- const tables = [
24
- this.table,
25
- ...(this.query.joins?.map((j) => j.target.tableData) ?? []),
26
- ]
27
- return Mappings.results(tables, rows)
18
+ return Mappings.results(
19
+ { name: "", columns: Projection.columns(this.query), constraints: [] },
20
+ rows,
21
+ )
28
22
  }
29
23
 
30
- async first(): Promise<any> {
24
+ async first() {
31
25
  const results = await this.fetch()
32
26
  if (results[0] === undefined) {
33
27
  throw new Error(
34
- `Query did not return a result (table: "${this.table.name}").`,
28
+ `Query did not return a result (table: "${this.query.source.tableData.name}").`,
35
29
  )
36
30
  }
37
31
  return results[0]