@carbonorm/carbonnode 3.10.0 → 4.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.
- package/dist/api/C6Constants.d.ts +20 -0
- package/dist/api/orm/builders/ConditionBuilder.d.ts +8 -0
- package/dist/api/orm/builders/JoinBuilder.d.ts +9 -0
- package/dist/api/orm/queries/PostQueryBuilder.d.ts +1 -1
- package/dist/api/restOrm.d.ts +4 -4
- package/dist/api/types/ormInterfaces.d.ts +32 -12
- package/dist/api/utils/cacheManager.d.ts +7 -8
- package/dist/index.cjs.js +566 -142
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +562 -141
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/cacheManager.test.ts +67 -0
- package/src/__tests__/expressServer.e2e.test.ts +104 -2
- package/src/__tests__/fixtures/c6.fixture.ts +5 -0
- package/src/__tests__/httpExecutorSingular.e2e.test.ts +35 -4
- package/src/__tests__/sakila-db/C6.js +1 -1
- package/src/__tests__/sakila-db/C6.ts +1 -1
- package/src/__tests__/sqlBuilders.complex.test.ts +85 -0
- package/src/__tests__/sqlBuilders.test.ts +28 -0
- package/src/api/C6Constants.ts +12 -2
- package/src/api/axiosInstance.ts +29 -0
- package/src/api/executors/HttpExecutor.ts +73 -97
- package/src/api/handlers/ExpressHandler.ts +30 -7
- package/src/api/orm/builders/ConditionBuilder.ts +227 -0
- package/src/api/orm/builders/JoinBuilder.ts +150 -1
- package/src/api/orm/queries/PostQueryBuilder.ts +4 -2
- package/src/api/orm/queries/SelectQueryBuilder.ts +5 -0
- package/src/api/orm/queries/UpdateQueryBuilder.ts +3 -1
- package/src/api/types/ormInterfaces.ts +32 -18
- package/src/api/utils/cacheManager.ts +75 -34
- package/src/api/utils/testHelpers.ts +5 -3
- package/src/variables/isNode.ts +1 -8
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AxiosPromise } from "axios";
|
|
2
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
apiRequestCache,
|
|
5
|
+
checkCache,
|
|
6
|
+
clearCache,
|
|
7
|
+
setCache,
|
|
8
|
+
} from "../api/utils/cacheManager";
|
|
9
|
+
import { checkAllRequestsComplete } from "../api/utils/testHelpers";
|
|
10
|
+
|
|
11
|
+
describe("cacheManager with map storage", () => {
|
|
12
|
+
const requestData = { id: 1 } as const;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
clearCache({ ignoreWarning: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("stores and returns cached requests", () => {
|
|
19
|
+
const mockRequest = Promise.resolve({ data: { rest: [] } }) as AxiosPromise;
|
|
20
|
+
|
|
21
|
+
setCache("GET", "table", requestData, {
|
|
22
|
+
requestArgumentsSerialized: "serialized",
|
|
23
|
+
request: mockRequest,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const cached = checkCache("GET", "table", { ...requestData });
|
|
27
|
+
expect(cached).toBe(mockRequest);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("clears cache entries", () => {
|
|
31
|
+
const mockRequest = Promise.resolve({ data: { rest: [] } }) as AxiosPromise;
|
|
32
|
+
|
|
33
|
+
setCache("GET", "table", requestData, {
|
|
34
|
+
requestArgumentsSerialized: "serialized",
|
|
35
|
+
request: mockRequest,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
clearCache({ ignoreWarning: true });
|
|
39
|
+
|
|
40
|
+
expect(apiRequestCache.size).toBe(0);
|
|
41
|
+
expect(checkCache("GET", "table", requestData)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("reports pending and completed requests via helper", () => {
|
|
45
|
+
const originalDocument = (global as any).document;
|
|
46
|
+
(global as any).document = {};
|
|
47
|
+
|
|
48
|
+
setCache("GET", "table", requestData, {
|
|
49
|
+
requestArgumentsSerialized: "pending",
|
|
50
|
+
request: Promise.resolve({ data: null }) as AxiosPromise,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(checkAllRequestsComplete()).toEqual(["pending"]);
|
|
54
|
+
|
|
55
|
+
setCache("GET", "table", requestData, {
|
|
56
|
+
requestArgumentsSerialized: "pending",
|
|
57
|
+
request: Promise.resolve({ data: null }) as AxiosPromise,
|
|
58
|
+
response: {} as any,
|
|
59
|
+
final: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(checkAllRequestsComplete()).toBe(true);
|
|
63
|
+
|
|
64
|
+
(global as any).document = originalDocument;
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
@@ -2,7 +2,7 @@ import mysql from "mysql2/promise";
|
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
import { AddressInfo } from "net";
|
|
4
4
|
import {describe, it, expect, beforeAll, afterAll} from "vitest";
|
|
5
|
-
import {Actor, C6, GLOBAL_REST_PARAMETERS} from "./sakila-db/C6.js";
|
|
5
|
+
import {Actor, C6, Film_Actor, GLOBAL_REST_PARAMETERS} from "./sakila-db/C6.js";
|
|
6
6
|
import {C6C} from "../api/C6Constants";
|
|
7
7
|
import createTestServer from "./fixtures/createTestServer";
|
|
8
8
|
|
|
@@ -23,7 +23,12 @@ beforeAll(async () => {
|
|
|
23
23
|
const {port} = server.address() as AddressInfo;
|
|
24
24
|
|
|
25
25
|
GLOBAL_REST_PARAMETERS.restURL = `http://127.0.0.1:${port}/rest/`;
|
|
26
|
-
|
|
26
|
+
const axiosClient = axios.create();
|
|
27
|
+
axiosClient.interceptors.response.use(
|
|
28
|
+
response => response,
|
|
29
|
+
error => Promise.reject(new Error(error?.message ?? 'Request failed')),
|
|
30
|
+
);
|
|
31
|
+
GLOBAL_REST_PARAMETERS.axios = axiosClient;
|
|
27
32
|
GLOBAL_REST_PARAMETERS.verbose = false;
|
|
28
33
|
// ensure HTTP executor is used
|
|
29
34
|
// @ts-ignore
|
|
@@ -93,6 +98,79 @@ describe("ExpressHandler e2e", () => {
|
|
|
93
98
|
expect(data?.rest.length).toBe(0);
|
|
94
99
|
});
|
|
95
100
|
|
|
101
|
+
it("stringifies plain object values in PUT updates", async () => {
|
|
102
|
+
const first_name = `Json${Date.now()}`;
|
|
103
|
+
const last_name = `User${Date.now()}`;
|
|
104
|
+
|
|
105
|
+
await Actor.Post({
|
|
106
|
+
first_name,
|
|
107
|
+
last_name,
|
|
108
|
+
} as any);
|
|
109
|
+
|
|
110
|
+
const payload = { greeting: "hello", flags: [1, true] };
|
|
111
|
+
|
|
112
|
+
let data = await Actor.Get({
|
|
113
|
+
[C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
|
|
114
|
+
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
115
|
+
} as any);
|
|
116
|
+
|
|
117
|
+
const actorId = data?.rest?.[0]?.actor_id;
|
|
118
|
+
expect(actorId).toBeTruthy();
|
|
119
|
+
|
|
120
|
+
await Actor.Put({
|
|
121
|
+
[C6C.WHERE]: { [Actor.ACTOR_ID]: actorId },
|
|
122
|
+
[C6C.UPDATE]: { first_name: payload },
|
|
123
|
+
} as any);
|
|
124
|
+
|
|
125
|
+
data = await Actor.Get({
|
|
126
|
+
[C6C.WHERE]: { [Actor.ACTOR_ID]: actorId },
|
|
127
|
+
} as any);
|
|
128
|
+
|
|
129
|
+
expect(data?.rest?.[0]?.first_name).toBe(JSON.stringify(payload));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("rejects operator-like objects in PUT updates", async () => {
|
|
133
|
+
const first_name = `Invalid${Date.now()}`;
|
|
134
|
+
const last_name = `User${Date.now()}`;
|
|
135
|
+
|
|
136
|
+
await Actor.Post({
|
|
137
|
+
first_name,
|
|
138
|
+
last_name,
|
|
139
|
+
} as any);
|
|
140
|
+
|
|
141
|
+
const data = await Actor.Get({
|
|
142
|
+
[C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
|
|
143
|
+
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
144
|
+
} as any);
|
|
145
|
+
|
|
146
|
+
const actorId = data?.rest?.[0]?.actor_id;
|
|
147
|
+
expect(actorId).toBeTruthy();
|
|
148
|
+
|
|
149
|
+
const operatorLike = { [C6C.GREATER_THAN]: "oops" } as any;
|
|
150
|
+
|
|
151
|
+
const prevRestUrl = GLOBAL_REST_PARAMETERS.restURL;
|
|
152
|
+
const prevAxios = GLOBAL_REST_PARAMETERS.axios;
|
|
153
|
+
GLOBAL_REST_PARAMETERS.mysqlPool = pool as any;
|
|
154
|
+
delete (GLOBAL_REST_PARAMETERS as any).restURL;
|
|
155
|
+
delete (GLOBAL_REST_PARAMETERS as any).axios;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
await Actor.Put({
|
|
159
|
+
[C6C.WHERE]: { [Actor.ACTOR_ID]: actorId },
|
|
160
|
+
[C6C.UPDATE]: { first_name: operatorLike },
|
|
161
|
+
} as any);
|
|
162
|
+
throw new Error('Expected PUT to reject for operator-like payload.');
|
|
163
|
+
} catch (error: any) {
|
|
164
|
+
const message = String(error?.message ?? error);
|
|
165
|
+
expect(message).toMatch(/operand/i);
|
|
166
|
+
} finally {
|
|
167
|
+
GLOBAL_REST_PARAMETERS.restURL = prevRestUrl;
|
|
168
|
+
GLOBAL_REST_PARAMETERS.axios = prevAxios;
|
|
169
|
+
// @ts-ignore
|
|
170
|
+
delete GLOBAL_REST_PARAMETERS.mysqlPool;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
96
174
|
it("respects METHOD=GET override on POST", async () => {
|
|
97
175
|
const {restURL} = GLOBAL_REST_PARAMETERS;
|
|
98
176
|
const table = Actor.TABLE_NAME;
|
|
@@ -121,4 +199,28 @@ describe("ExpressHandler e2e", () => {
|
|
|
121
199
|
expect(response.status).toBe(200);
|
|
122
200
|
expect(response.data?.success).toBeTruthy();
|
|
123
201
|
});
|
|
202
|
+
|
|
203
|
+
it("allows composite keys when a URL primary is present", async () => {
|
|
204
|
+
const {restURL} = GLOBAL_REST_PARAMETERS;
|
|
205
|
+
const table = Film_Actor.TABLE_NAME;
|
|
206
|
+
|
|
207
|
+
const seed = await Film_Actor.Get({
|
|
208
|
+
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
209
|
+
} as any);
|
|
210
|
+
|
|
211
|
+
const filmActor = seed.rest[0];
|
|
212
|
+
expect(filmActor).toBeTruthy();
|
|
213
|
+
|
|
214
|
+
const response = await axios.post(`${restURL}${table}/${filmActor.actor_id}?METHOD=GET`, {
|
|
215
|
+
[C6C.WHERE]: {
|
|
216
|
+
[Film_Actor.ACTOR_ID]: filmActor.actor_id,
|
|
217
|
+
[Film_Actor.FILM_ID]: filmActor.film_id,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
expect(response.status).toBe(200);
|
|
222
|
+
expect(response.data?.rest).toHaveLength(1);
|
|
223
|
+
expect(response.data?.rest?.[0]?.actor_id).toBe(filmActor.actor_id);
|
|
224
|
+
expect(response.data?.rest?.[0]?.film_id).toBe(filmActor.film_id);
|
|
225
|
+
});
|
|
124
226
|
});
|
|
@@ -43,6 +43,7 @@ export function buildTestConfig() {
|
|
|
43
43
|
'actor.first_name': 'first_name',
|
|
44
44
|
'actor.last_name': 'last_name',
|
|
45
45
|
'actor.binarycol': 'binarycol',
|
|
46
|
+
'actor.json_data': 'json_data',
|
|
46
47
|
} as const;
|
|
47
48
|
|
|
48
49
|
const filmActorCols = {
|
|
@@ -62,6 +63,7 @@ export function buildTestConfig() {
|
|
|
62
63
|
|
|
63
64
|
// Special-case: mark binary column as BINARY to test conversion
|
|
64
65
|
C6.TABLES.actor.TYPE_VALIDATION['binarycol'].MYSQL_TYPE = 'BINARY(16)';
|
|
66
|
+
C6.TABLES.actor.TYPE_VALIDATION['json_data'].MYSQL_TYPE = 'JSON';
|
|
65
67
|
|
|
66
68
|
const baseConfig: iRest<any, any, any> = {
|
|
67
69
|
C6,
|
|
@@ -135,6 +137,7 @@ export function buildParcelConfig() {
|
|
|
135
137
|
'property_units.unit_id': 'unit_id',
|
|
136
138
|
'property_units.location': 'location',
|
|
137
139
|
'property_units.parcel_id': 'parcel_id',
|
|
140
|
+
'property_units.county_id': 'county_id',
|
|
138
141
|
} as const;
|
|
139
142
|
|
|
140
143
|
const parcelSalesCols = {
|
|
@@ -146,6 +149,8 @@ export function buildParcelConfig() {
|
|
|
146
149
|
|
|
147
150
|
const parcelBuildingCols = {
|
|
148
151
|
'parcel_building_details.parcel_id': 'parcel_id',
|
|
152
|
+
'parcel_building_details.year_built': 'year_built',
|
|
153
|
+
'parcel_building_details.gla': 'gla',
|
|
149
154
|
} as const;
|
|
150
155
|
|
|
151
156
|
const C6 = {
|
|
@@ -23,7 +23,12 @@ beforeAll(async () => {
|
|
|
23
23
|
const { port } = server.address() as AddressInfo;
|
|
24
24
|
|
|
25
25
|
GLOBAL_REST_PARAMETERS.restURL = `http://127.0.0.1:${port}/rest/`;
|
|
26
|
-
|
|
26
|
+
const axiosClient = axios.create();
|
|
27
|
+
axiosClient.interceptors.response.use(
|
|
28
|
+
response => response,
|
|
29
|
+
error => Promise.reject(new Error(error?.message ?? "Request failed")),
|
|
30
|
+
);
|
|
31
|
+
GLOBAL_REST_PARAMETERS.axios = axiosClient;
|
|
27
32
|
GLOBAL_REST_PARAMETERS.verbose = false;
|
|
28
33
|
// ensure HTTP executor is used
|
|
29
34
|
// @ts-ignore
|
|
@@ -41,14 +46,14 @@ describe("HttpExecutor singular e2e", () => {
|
|
|
41
46
|
const last_name = `User${Date.now()}`;
|
|
42
47
|
|
|
43
48
|
// POST
|
|
44
|
-
|
|
45
|
-
expect(postRes.affected).toBe(1);
|
|
49
|
+
await Actor.Post({ first_name, last_name } as any);
|
|
46
50
|
|
|
47
51
|
// Fetch inserted id using complex query
|
|
48
52
|
let data = await Actor.Get({
|
|
49
53
|
[C6C.WHERE]: { [Actor.FIRST_NAME]: first_name, [Actor.LAST_NAME]: last_name },
|
|
50
54
|
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
51
|
-
}
|
|
55
|
+
});
|
|
56
|
+
|
|
52
57
|
expect(data.rest).toHaveLength(1);
|
|
53
58
|
const testId = data.rest[0].actor_id;
|
|
54
59
|
|
|
@@ -89,4 +94,30 @@ describe("HttpExecutor singular e2e", () => {
|
|
|
89
94
|
expect(Array.isArray(data.rest)).toBe(true);
|
|
90
95
|
expect(data.rest.length).toBe(0);
|
|
91
96
|
});
|
|
97
|
+
|
|
98
|
+
it("exposes next when pagination continues", async () => {
|
|
99
|
+
const data = await Actor.Get({
|
|
100
|
+
[C6C.PAGINATION]: { [C6C.LIMIT]: 2 },
|
|
101
|
+
} as any);
|
|
102
|
+
|
|
103
|
+
expect(Array.isArray(data.rest)).toBe(true);
|
|
104
|
+
expect(data.rest).toHaveLength(2);
|
|
105
|
+
expect(typeof data.next).toBe("function");
|
|
106
|
+
|
|
107
|
+
const nextPage = await data.next?.();
|
|
108
|
+
expect(nextPage?.rest).toBeDefined();
|
|
109
|
+
expect(Array.isArray(nextPage?.rest)).toBe(true);
|
|
110
|
+
expect(nextPage?.rest?.length).toBeGreaterThan(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("exposes limit 1 does not expose next", async () => {
|
|
114
|
+
const data = await Actor.Get({
|
|
115
|
+
[C6C.PAGINATION]: { [C6C.LIMIT]: 1 },
|
|
116
|
+
} as any);
|
|
117
|
+
|
|
118
|
+
expect(Array.isArray(data.rest)).toBe(true);
|
|
119
|
+
expect(data.rest).toHaveLength(1);
|
|
120
|
+
expect(typeof data.next).toBe("undefined");
|
|
121
|
+
|
|
122
|
+
});
|
|
92
123
|
});
|
|
@@ -1342,7 +1342,7 @@ export const TABLES = {
|
|
|
1342
1342
|
};
|
|
1343
1343
|
export const C6 = {
|
|
1344
1344
|
...C6Constants,
|
|
1345
|
-
C6VERSION: '
|
|
1345
|
+
C6VERSION: '4.0.0',
|
|
1346
1346
|
IMPORT: async (tableName) => {
|
|
1347
1347
|
tableName = tableName.toLowerCase();
|
|
1348
1348
|
// if tableName is not a key in the TABLES object then throw an error
|
|
@@ -2009,7 +2009,7 @@ export type RestTableInterfaces = iActor
|
|
|
2009
2009
|
|
|
2010
2010
|
export const C6 : iC6Object<RestTableInterfaces> = {
|
|
2011
2011
|
...C6Constants,
|
|
2012
|
-
C6VERSION: '
|
|
2012
|
+
C6VERSION: '4.0.0',
|
|
2013
2013
|
IMPORT: async (tableName: string) : Promise<iDynamicApiImport> => {
|
|
2014
2014
|
|
|
2015
2015
|
tableName = tableName.toLowerCase();
|
|
@@ -9,6 +9,7 @@ const Property_Units = {
|
|
|
9
9
|
UNIT_ID: 'property_units.unit_id',
|
|
10
10
|
LOCATION: 'property_units.location',
|
|
11
11
|
PARCEL_ID: 'property_units.parcel_id',
|
|
12
|
+
COUNTY_ID: 'property_units.county_id',
|
|
12
13
|
} as const;
|
|
13
14
|
|
|
14
15
|
const Parcel_Sales = {
|
|
@@ -19,6 +20,13 @@ const Parcel_Sales = {
|
|
|
19
20
|
SALE_DATE: 'parcel_sales.sale_date',
|
|
20
21
|
} as const;
|
|
21
22
|
|
|
23
|
+
const Parcel_Building_Details = {
|
|
24
|
+
TABLE_NAME: 'parcel_building_details',
|
|
25
|
+
PARCEL_ID: 'parcel_building_details.parcel_id',
|
|
26
|
+
YEAR_BUILT: 'parcel_building_details.year_built',
|
|
27
|
+
GLA: 'parcel_building_details.gla',
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
22
30
|
/**
|
|
23
31
|
* Complex SELECT coverage focused on WHERE operators, JOIN chains, ORDER, and pagination.
|
|
24
32
|
*/
|
|
@@ -479,4 +487,81 @@ describe('SQL Builders - Complex SELECTs', () => {
|
|
|
479
487
|
expect(sql).toContain('WHERE ( property_units.unit_id IN (SELECT parcel_sales.parcel_id');
|
|
480
488
|
expect(params).toContain(5000);
|
|
481
489
|
});
|
|
490
|
+
|
|
491
|
+
it('serializes spatial filtering with FORCE INDEX and correlated EXISTS subqueries', () => {
|
|
492
|
+
const config = buildParcelConfig();
|
|
493
|
+
const polygon = 'POLYGON((39.5185659 -105.0142915, 39.5401859 -105.0142915, 39.5401859 -104.9862115, 39.5185659 -104.9862115, 39.5185659 -105.0142915))';
|
|
494
|
+
const point = [C6C.ST_GEOMFROMTEXT, ['POINT(39.5293759 -105.0002515)', 4326]];
|
|
495
|
+
const unitId = Buffer.from('11F0615D24861BE1ADD40AFFCF6A1F27', 'hex');
|
|
496
|
+
const countyId = Buffer.from('11F012CFF561A29DBB0E0AFFF25F1747', 'hex');
|
|
497
|
+
|
|
498
|
+
const qb = new SelectQueryBuilder(config as any, {
|
|
499
|
+
[C6C.SELECT]: ['property_units.*'],
|
|
500
|
+
[C6C.INDEX_HINTS]: {
|
|
501
|
+
[C6C.FORCE_INDEX]: ['idx_county_id', 'idx_property_units_location'],
|
|
502
|
+
},
|
|
503
|
+
[C6C.WHERE]: {
|
|
504
|
+
[Property_Units.UNIT_ID]: [C6C.NOT_EQUAL, unitId],
|
|
505
|
+
[Property_Units.COUNTY_ID]: countyId,
|
|
506
|
+
[C6C.MBRCONTAINS]: [
|
|
507
|
+
[C6C.ST_GEOMFROMTEXT, [polygon, 4326]],
|
|
508
|
+
Property_Units.LOCATION,
|
|
509
|
+
],
|
|
510
|
+
[C6C.LESS_THAN_OR_EQUAL_TO]: [
|
|
511
|
+
[C6C.ST_DISTANCE_SPHERE, Property_Units.LOCATION, point],
|
|
512
|
+
1200,
|
|
513
|
+
],
|
|
514
|
+
[C6C.EXISTS]: [
|
|
515
|
+
[
|
|
516
|
+
Property_Units.PARCEL_ID,
|
|
517
|
+
{
|
|
518
|
+
[C6C.SUBSELECT]: {
|
|
519
|
+
[C6C.SELECT]: [Parcel_Building_Details.PARCEL_ID],
|
|
520
|
+
[C6C.FROM]: Parcel_Building_Details.TABLE_NAME,
|
|
521
|
+
[C6C.WHERE]: {
|
|
522
|
+
[Parcel_Building_Details.YEAR_BUILT]: [C6C.BETWEEN, [1988, 2008]],
|
|
523
|
+
[Parcel_Building_Details.GLA]: [C6C.BETWEEN, [1876.5, 3127.5]],
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
],
|
|
528
|
+
[
|
|
529
|
+
Property_Units.PARCEL_ID,
|
|
530
|
+
{
|
|
531
|
+
[C6C.SUBSELECT]: {
|
|
532
|
+
[C6C.SELECT]: [Parcel_Sales.PARCEL_ID],
|
|
533
|
+
[C6C.FROM]: Parcel_Sales.TABLE_NAME,
|
|
534
|
+
[C6C.WHERE]: {
|
|
535
|
+
[Parcel_Sales.SALE_DATE]: [C6C.BETWEEN, ['2023-01-01', '2024-06-30']],
|
|
536
|
+
[Parcel_Sales.SALE_PRICE]: [C6C.NOT_EQUAL, 0],
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
],
|
|
542
|
+
},
|
|
543
|
+
[C6C.PAGINATION]: {
|
|
544
|
+
[C6C.LIMIT]: 100,
|
|
545
|
+
[C6C.ORDER]: {
|
|
546
|
+
[C6C.ST_DISTANCE_SPHERE]: [Property_Units.LOCATION, point],
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
} as any, false);
|
|
550
|
+
|
|
551
|
+
const { sql, params } = qb.build(Property_Units.TABLE_NAME);
|
|
552
|
+
|
|
553
|
+
expect(sql).toContain('FORCE INDEX (`idx_county_id`, `idx_property_units_location`)');
|
|
554
|
+
expect(sql).toMatch(/MBRCONTAINS\(ST_GEOMFROMTEXT\('POLYGON\(\(39\.5185659 -105\.0142915, 39\.5401859 -105\.0142915, 39\.5401859 -104\.9862115, 39\.5185659 -104\.9862115, 39\.5185659 -105\.0142915\)\)', 4326\), property_units\.location\)/);
|
|
555
|
+
expect(sql).toMatch(/ST_DISTANCE_SPHERE\(property_units\.location, ST_GEOMFROMTEXT\('POINT\(39\.5293759 -105\.0002515\)', 4326\)\) <= \?/);
|
|
556
|
+
expect(sql).toMatch(/\(parcel_building_details\.parcel_id\) = property_units\.parcel_id/);
|
|
557
|
+
expect(sql).toMatch(/\(parcel_sales\.parcel_id\) = property_units\.parcel_id/);
|
|
558
|
+
expect(sql).toMatch(/ORDER BY ST[_]Distance[_]Sphere\(property_units\.location, ST_GEOMFROMTEXT\('POINT\(39\.5293759 -105\.0002515\)', 4326\)\)/i);
|
|
559
|
+
|
|
560
|
+
expect(params).toHaveLength(10);
|
|
561
|
+
expect(params[0]).toEqual(unitId);
|
|
562
|
+
expect(params[1]).toEqual(countyId);
|
|
563
|
+
expect(params).toContain(1200);
|
|
564
|
+
expect(params.slice(3, 7)).toEqual([1988, 2008, 1876.5, 3127.5]);
|
|
565
|
+
expect(params.slice(7)).toEqual(['2023-01-01', '2024-06-30', 0]);
|
|
566
|
+
});
|
|
482
567
|
});
|
|
@@ -61,6 +61,34 @@ describe('SQL Builders', () => {
|
|
|
61
61
|
expect(params).toEqual(['BOB', 'SMITH']);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
it('stringifies plain object inserts for JSON columns', () => {
|
|
65
|
+
const config = buildTestConfig();
|
|
66
|
+
const payload = { hello: 'world', nested: { ok: true } };
|
|
67
|
+
const qb = new PostQueryBuilder(config as any, {
|
|
68
|
+
[C6C.INSERT]: {
|
|
69
|
+
'actor.json_data': payload,
|
|
70
|
+
},
|
|
71
|
+
} as any, false);
|
|
72
|
+
|
|
73
|
+
const { sql, params } = qb.build('actor');
|
|
74
|
+
|
|
75
|
+
expect(sql).toContain('`json_data`');
|
|
76
|
+
expect(params).toEqual([JSON.stringify(payload)]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('throws on operator-shaped insert payloads', () => {
|
|
80
|
+
const config = buildTestConfig();
|
|
81
|
+
const qb = new PostQueryBuilder(config as any, {
|
|
82
|
+
[C6C.INSERT]: {
|
|
83
|
+
'actor.first_name': {
|
|
84
|
+
[C6C.GREATER_THAN]: 'ALICE',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
} as any, false);
|
|
88
|
+
|
|
89
|
+
expect(() => qb.build('actor')).toThrowError(/requires two operands/);
|
|
90
|
+
});
|
|
91
|
+
|
|
64
92
|
it('builds UPDATE with WHERE and pagination', () => {
|
|
65
93
|
const config = buildTestConfig();
|
|
66
94
|
const qb = new UpdateQueryBuilder(config as any, {
|
package/src/api/C6Constants.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
1
|
export const C6Constants = {
|
|
3
|
-
|
|
4
2
|
// try to 1=1 match the Rest abstract class
|
|
5
3
|
ADDDATE: 'ADDDATE',
|
|
6
4
|
ADDTIME: 'ADDTIME',
|
|
@@ -34,11 +32,13 @@ export const C6Constants = {
|
|
|
34
32
|
DESC: 'DESC',
|
|
35
33
|
DISTINCT: 'DISTINCT',
|
|
36
34
|
|
|
35
|
+
EXISTS: 'EXISTS',
|
|
37
36
|
EXTRACT: 'EXTRACT',
|
|
38
37
|
EQUAL: '=',
|
|
39
38
|
EQUAL_NULL_SAFE: '<=>',
|
|
40
39
|
|
|
41
40
|
FALSE: 'FALSE',
|
|
41
|
+
FORCE: 'FORCE',
|
|
42
42
|
FULL_OUTER: 'FULL_OUTER',
|
|
43
43
|
FROM_DAYS: 'FROM_DAYS',
|
|
44
44
|
FROM_UNIXTIME: 'FROM_UNIXTIME',
|
|
@@ -57,6 +57,7 @@ export const C6Constants = {
|
|
|
57
57
|
HOUR_MINUTE: 'HOUR_MINUTE',
|
|
58
58
|
|
|
59
59
|
IN: 'IN',
|
|
60
|
+
INDEX: 'INDEX',
|
|
60
61
|
IS: 'IS',
|
|
61
62
|
IS_NOT: 'IS_NOT',
|
|
62
63
|
INNER: 'INNER',
|
|
@@ -77,6 +78,7 @@ export const C6Constants = {
|
|
|
77
78
|
MAKEDATE: 'MAKEDATE',
|
|
78
79
|
MAKETIME: 'MAKETIME',
|
|
79
80
|
MATCH_AGAINST: 'MATCH_AGAINST',
|
|
81
|
+
MBRCONTAINS: 'MBRContains',
|
|
80
82
|
MONTHNAME: 'MONTHNAME',
|
|
81
83
|
MICROSECOND: 'MICROSECOND',
|
|
82
84
|
MINUTE: 'MINUTE',
|
|
@@ -95,8 +97,16 @@ export const C6Constants = {
|
|
|
95
97
|
ORDER: 'ORDER',
|
|
96
98
|
OR: 'OR',
|
|
97
99
|
|
|
100
|
+
INDEX_HINTS: 'INDEX_HINTS',
|
|
101
|
+
|
|
102
|
+
FORCE_INDEX: 'FORCE INDEX',
|
|
103
|
+
USE_INDEX: 'USE INDEX',
|
|
104
|
+
IGNORE_INDEX: 'IGNORE INDEX',
|
|
105
|
+
|
|
98
106
|
PAGE: 'PAGE',
|
|
99
107
|
PAGINATION: 'PAGINATION',
|
|
108
|
+
POLYGON: 'POLYGON',
|
|
109
|
+
POINT: 'POINT',
|
|
100
110
|
RIGHT_OUTER: 'RIGHT_OUTER',
|
|
101
111
|
|
|
102
112
|
SECOND: 'SECOND',
|
package/src/api/axiosInstance.ts
CHANGED
|
@@ -151,7 +151,36 @@ axiosInstance.interceptors.request.use((config) => {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
(config as any).__carbonStart = performance.now();
|
|
155
|
+
|
|
154
156
|
return config;
|
|
155
157
|
});
|
|
156
158
|
|
|
159
|
+
|
|
160
|
+
axiosInstance.interceptors.response.use(
|
|
161
|
+
(response) => {
|
|
162
|
+
const end = performance.now();
|
|
163
|
+
const start = (response.config as any).__carbonStart;
|
|
164
|
+
(response as any).__carbonTiming = {
|
|
165
|
+
start,
|
|
166
|
+
end,
|
|
167
|
+
duration: end - start,
|
|
168
|
+
};
|
|
169
|
+
return response;
|
|
170
|
+
},
|
|
171
|
+
(error) => {
|
|
172
|
+
if (error.config) {
|
|
173
|
+
const end = performance.now();
|
|
174
|
+
const start = (error.config as any).__carbonStart;
|
|
175
|
+
(error as any).__carbonTiming = {
|
|
176
|
+
start,
|
|
177
|
+
end,
|
|
178
|
+
duration: end - start,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
|
|
157
186
|
export default axiosInstance
|