Skip to content

Commit 31ce1e1

Browse files
authored
Merge pull request #6 from StrontiumJS/master
sync
2 parents 6b66984 + efc5c22 commit 31ce1e1

File tree

7 files changed

+270
-12
lines changed

7 files changed

+270
-12
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "strontium",
3-
"version": "2.7.7",
3+
"version": "2.8.0",
44
"description": "Strontium is a TypeScript toolkit for High Performance API servers built for Production not Projects.",
55
"main": "lib/src/index.js",
66
"types": "lib/src/index.d.ts",

src/http/abstract/RouterMap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export type RouterMap = Array<{
55
method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE" | "OPTIONS"
66
route: string
77
endpointController: ConstructorOf<EndpointController>
8+
metadata?: { [key: string]: any }
89
}>

src/http/drivers/FastifyServer.ts

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,22 @@ export class FastifyServer implements Process {
2121
private port: number = 8080,
2222
private host: string = "127.0.0.1"
2323
) {
24-
for (let route of routes) {
24+
/*
25+
To handle limitations in Find My Way (Fastify's internal routing library)
26+
Strontium provides a preprocess format for routes to prevent certain conflicts.
27+
*/
28+
let processedRoutes = FastifyServer.preProcessRoutes(routes)
29+
30+
for (let route of processedRoutes) {
2531
switch (route.method) {
2632
case "GET":
2733
this.server.get(
2834
route.route,
2935
this.requestHandler(
3036
route.endpointController,
3137
route.route,
32-
"GET"
38+
"GET",
39+
route.metadata
3340
)
3441
)
3542
break
@@ -39,7 +46,8 @@ export class FastifyServer implements Process {
3946
this.requestHandler(
4047
route.endpointController,
4148
route.route,
42-
"POST"
49+
"POST",
50+
route.metadata
4351
)
4452
)
4553
break
@@ -49,7 +57,8 @@ export class FastifyServer implements Process {
4957
this.requestHandler(
5058
route.endpointController,
5159
route.route,
52-
"PATCH"
60+
"PATCH",
61+
route.metadata
5362
)
5463
)
5564
break
@@ -59,7 +68,8 @@ export class FastifyServer implements Process {
5968
this.requestHandler(
6069
route.endpointController,
6170
route.route,
62-
"PUT"
71+
"PUT",
72+
route.metadata
6373
)
6474
)
6575
break
@@ -69,7 +79,8 @@ export class FastifyServer implements Process {
6979
this.requestHandler(
7080
route.endpointController,
7181
route.route,
72-
"DELETE"
82+
"DELETE",
83+
route.metadata
7384
)
7485
)
7586
break
@@ -79,14 +90,100 @@ export class FastifyServer implements Process {
7990
this.requestHandler(
8091
route.endpointController,
8192
route.route,
82-
"OPTIONS"
93+
"OPTIONS",
94+
route.metadata
8395
)
8496
)
8597
break
8698
}
8799
}
88100
}
89101

102+
public static preProcessRoutes(routes: RouterMap): RouterMap {
103+
let processedRoutes: RouterMap = []
104+
for (let route of routes) {
105+
// Check if there are any enum param blocks
106+
let enumeratedBlocks = route.route.match(
107+
/{([a-zA-Z0-9_-]*)\|([a-zA-Z0-9_,-]*)}/g
108+
)
109+
110+
if (enumeratedBlocks === null) {
111+
processedRoutes.push(route)
112+
} else {
113+
const generatePermutations = (
114+
index: number
115+
): Array<Array<string>> => {
116+
// Typescript correctly identifies this function may run when enumeratedBlocks is null
117+
// however within this context that is not possible - so ! to overcome
118+
let currentBlock = enumeratedBlocks![index]
119+
120+
let childPermutations: Array<Array<string>> = []
121+
if (index < enumeratedBlocks!.length - 1) {
122+
childPermutations = generatePermutations(index + 1)
123+
}
124+
125+
let permutations: Array<Array<string>> = []
126+
127+
// Use a more direct method because the absence of "matchAll" in Node would make the RegEx method uglier
128+
let fieldContents = currentBlock
129+
.replace("{", "")
130+
.replace("}", "")
131+
132+
let [
133+
fieldName,
134+
serializedFieldValues,
135+
] = fieldContents.split("|")
136+
let fieldValues = serializedFieldValues.split(",")
137+
138+
for (let value of fieldValues) {
139+
if (childPermutations.length === 0) {
140+
permutations.push([value])
141+
} else {
142+
for (let childPermutation of childPermutations) {
143+
permutations.push([value, ...childPermutation])
144+
}
145+
}
146+
}
147+
148+
return permutations
149+
}
150+
151+
let routePermutations = generatePermutations(0)
152+
153+
for (let permutation of routePermutations) {
154+
let pathPermutation = route.route
155+
let parameters: { [key: string]: string } = {
156+
...(route.metadata || {}),
157+
}
158+
159+
for (let i = 0; i < enumeratedBlocks.length; i++) {
160+
let currentBlock = enumeratedBlocks[i]
161+
let fieldContents = currentBlock
162+
.replace("{", "")
163+
.replace("}", "")
164+
let [fieldName] = fieldContents.split("|")
165+
166+
parameters[fieldName] = permutation[i]
167+
168+
pathPermutation = pathPermutation.replace(
169+
currentBlock,
170+
permutation[i]
171+
)
172+
}
173+
174+
processedRoutes.push({
175+
endpointController: route.endpointController,
176+
method: route.method,
177+
route: pathPermutation,
178+
metadata: parameters,
179+
})
180+
}
181+
}
182+
}
183+
184+
return processedRoutes
185+
}
186+
90187
public isHealthy(): boolean {
91188
return this.isAlive
92189
}
@@ -147,7 +244,8 @@ export class FastifyServer implements Process {
147244
protected requestHandler(
148245
controller: ConstructorOf<EndpointController>,
149246
path: string,
150-
method: string
247+
method: string,
248+
routeMetadata: { [key: string]: any } = {}
151249
): (
152250
request: Fastify.FastifyRequest<any, any, any, any, any>,
153251
response: Fastify.FastifyReply<any>
@@ -197,7 +295,10 @@ export class FastifyServer implements Process {
197295
headers: request.headers,
198296
query: request.query,
199297
params: request.params,
200-
meta: this.getRequestMetadata(request),
298+
meta: {
299+
...routeMetadata,
300+
...this.getRequestMetadata(request),
301+
},
201302
})
202303

203304
rawResponse = await endpointController.handle(validatedInput)

src/query/abstract/Filter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ export type FieldFilter<P extends keyof T, T> =
44
| {
55
$in?: Array<T[P]>
66
$nin?: Array<T[P]>
7+
$eq?: T[P]
8+
// TODO: Review if the null type on $neq is necessary
79
$neq?: T[P] | null
810
$gt?: T[P]
911
$gte?: T[P]
1012
$lt?: T[P]
1113
$lte?: T[P]
14+
$contains?: T[P]
1215
}
1316
| T[P]
1417

src/query/drivers/sql/SQLFilterCompiler.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ export const compileSQLFilter: FilterCompiler<[string, Array<any>]> = (
3939

4040
let subquery = filter[field]
4141

42-
if (subquery === null) {
42+
if (subquery === undefined) {
43+
continue
44+
} else if (subquery === null) {
4345
queries.push(["?? IS NULL", [field]])
4446
} else if (subquery.$in !== undefined) {
4547
if (subquery.$in.length === 0) {
@@ -76,6 +78,10 @@ export const compileSQLFilter: FilterCompiler<[string, Array<any>]> = (
7678
queries.push(["?? < ?", [field, subquery.$lt]])
7779
} else if (subquery.$lte !== undefined) {
7880
queries.push(["?? <= ?", [field, subquery.$lte]])
81+
} else if (subquery.$contains !== undefined) {
82+
queries.push(["?? LIKE ?", [field, `%${subquery.$contains}%`]])
83+
} else if (subquery.$eq !== undefined) {
84+
queries.push(["?? = ?", [field, subquery.$eq]])
7985
} else {
8086
queries.push(["?? = ?", [field, subquery]])
8187
}

src/query/drivers/sql/TableRepository.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "../../../datastore"
99
import { injectable } from "inversify"
1010
import { isUndefined, omitBy } from "lodash"
11+
import { Logger } from "../../../logging"
1112

1213
import { Filter, compileSQLFilter } from "../.."
1314

@@ -31,7 +32,8 @@ export abstract class TableRepository<
3132
protected store: SQLStore,
3233
protected tableName: string,
3334
protected queryFields: Array<keyof T>,
34-
protected primaryKeyField: K
35+
protected primaryKeyField: K,
36+
protected logger?: Logger
3537
) {
3638
super()
3739

@@ -114,6 +116,7 @@ export abstract class TableRepository<
114116
} = {},
115117
connection: SQLStore = this.store
116118
): Promise<Array<T>> {
119+
let startTime = process.hrtime()
117120
let [filterQuery, filterParameters] = compileSQLFilter(filter)
118121
let parameters = [this.tableName, ...filterParameters]
119122

@@ -150,6 +153,8 @@ export abstract class TableRepository<
150153
processedQuery,
151154
processedParameters
152155
)
156+
157+
this.recordQueryTime(processedQuery, startTime)
153158
return results
154159
}
155160

@@ -208,4 +213,25 @@ export abstract class TableRepository<
208213
async generateID(): Promise<any> {
209214
return undefined
210215
}
216+
217+
protected recordQueryTime(
218+
queryString: string,
219+
startTime: [number, number]
220+
): void {
221+
if (this.logger !== undefined) {
222+
let runtime = process.hrtime(startTime)
223+
224+
this.logger.debug(
225+
`[REPOSITORY - QUERY - DIAGNOSTICS] ${
226+
this.tableName
227+
} query complete - ${runtime[0]} s and ${runtime[1] /
228+
1000000} ms`,
229+
{
230+
query: queryString,
231+
seconds: runtime[0],
232+
milliseconds: runtime[1] / 1000000,
233+
}
234+
)
235+
}
236+
}
211237
}

0 commit comments

Comments
 (0)