@loomcore/api 0.1.70 → 0.1.73

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 (26) hide show
  1. package/dist/__tests__/postgres-test-migrations/postgres-test-schema.js +123 -2
  2. package/dist/databases/mongo-db/utils/convert-operations-to-pipeline.util.js +190 -2
  3. package/dist/databases/operations/__tests__/models/agent.model.d.ts +25 -0
  4. package/dist/databases/operations/__tests__/models/agent.model.js +8 -0
  5. package/dist/databases/operations/__tests__/models/client-report.model.d.ts +22 -2
  6. package/dist/databases/operations/__tests__/models/client-report.model.js +3 -1
  7. package/dist/databases/operations/__tests__/models/district.model.d.ts +12 -0
  8. package/dist/databases/operations/__tests__/models/district.model.js +7 -0
  9. package/dist/databases/operations/__tests__/models/person.model.d.ts +2 -0
  10. package/dist/databases/operations/__tests__/models/school.model.d.ts +12 -0
  11. package/dist/databases/operations/__tests__/models/school.model.js +7 -0
  12. package/dist/databases/operations/__tests__/models/state.model.d.ts +8 -0
  13. package/dist/databases/operations/__tests__/models/state.model.js +6 -0
  14. package/dist/databases/operations/index.d.ts +1 -0
  15. package/dist/databases/operations/index.js +1 -0
  16. package/dist/databases/operations/join-through-many.operation.d.ts +10 -0
  17. package/dist/databases/operations/join-through-many.operation.js +18 -0
  18. package/dist/databases/operations/operation.d.ts +2 -1
  19. package/dist/databases/postgres/commands/postgres-batch-update.command.js +2 -1
  20. package/dist/databases/postgres/queries/postgres-get-all.query.js +2 -1
  21. package/dist/databases/postgres/queries/postgres-get-by-id.query.js +2 -1
  22. package/dist/databases/postgres/queries/postgres-get.query.js +2 -1
  23. package/dist/databases/postgres/utils/build-join-clauses.js +78 -42
  24. package/dist/databases/postgres/utils/build-select-clause.js +5 -0
  25. package/dist/databases/postgres/utils/transform-join-results.js +106 -22
  26. package/package.json +1 -1
@@ -123,7 +123,30 @@ export const getPostgresTestSchema = (config) => {
123
123
  }
124
124
  });
125
125
  migrations.push({
126
- name: '00000000000104_schema-clients',
126
+ name: '00000000000105_5_schema-agents',
127
+ up: async ({ context: pool }) => {
128
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
129
+ await pool.query(`
130
+ CREATE TABLE IF NOT EXISTS "agents" (
131
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
132
+ ${orgColumnDef}
133
+ "person_id" INTEGER NOT NULL UNIQUE,
134
+ "_created" TIMESTAMPTZ NOT NULL,
135
+ "_createdBy" INTEGER NOT NULL,
136
+ "_updated" TIMESTAMPTZ NOT NULL,
137
+ "_updatedBy" INTEGER NOT NULL,
138
+ "_deleted" TIMESTAMPTZ,
139
+ "_deletedBy" INTEGER,
140
+ CONSTRAINT fk_agents_person_id FOREIGN KEY ("person_id") REFERENCES persons("_id") ON DELETE CASCADE
141
+ )
142
+ `);
143
+ },
144
+ down: async ({ context: pool }) => {
145
+ await pool.query('DROP TABLE IF EXISTS "agents"');
146
+ }
147
+ });
148
+ migrations.push({
149
+ name: '00000000000105_6_schema-clients',
127
150
  up: async ({ context: pool }) => {
128
151
  const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
129
152
  await pool.query(`
@@ -132,13 +155,15 @@ export const getPostgresTestSchema = (config) => {
132
155
  ${orgColumnDef}
133
156
  "external_id" VARCHAR UNIQUE,
134
157
  "person_id" INTEGER NOT NULL UNIQUE,
158
+ "agent_id" INTEGER,
135
159
  "_created" TIMESTAMPTZ NOT NULL,
136
160
  "_createdBy" INTEGER NOT NULL,
137
161
  "_updated" TIMESTAMPTZ NOT NULL,
138
162
  "_updatedBy" INTEGER NOT NULL,
139
163
  "_deleted" TIMESTAMPTZ,
140
164
  "_deletedBy" INTEGER,
141
- CONSTRAINT fk_clients_person_id FOREIGN KEY ("person_id") REFERENCES persons("_id") ON DELETE CASCADE
165
+ CONSTRAINT fk_clients_person_id FOREIGN KEY ("person_id") REFERENCES persons("_id") ON DELETE CASCADE,
166
+ CONSTRAINT fk_clients_agent_id FOREIGN KEY ("agent_id") REFERENCES agents("_id") ON DELETE SET NULL
142
167
  )
143
168
  `);
144
169
  },
@@ -279,5 +304,101 @@ export const getPostgresTestSchema = (config) => {
279
304
  await pool.query('DROP TABLE IF EXISTS "persons_phone_numbers"');
280
305
  }
281
306
  });
307
+ migrations.push({
308
+ name: '00000000000111_schema-states',
309
+ up: async ({ context: pool }) => {
310
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
311
+ await pool.query(`
312
+ CREATE TABLE IF NOT EXISTS states (
313
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
314
+ ${orgColumnDef}
315
+ "name" VARCHAR NOT NULL,
316
+ "_created" TIMESTAMPTZ NOT NULL,
317
+ "_createdBy" INTEGER NOT NULL,
318
+ "_updated" TIMESTAMPTZ NOT NULL,
319
+ "_updatedBy" INTEGER NOT NULL,
320
+ "_deleted" TIMESTAMPTZ,
321
+ "_deletedBy" INTEGER
322
+ )
323
+ `);
324
+ },
325
+ down: async ({ context: pool }) => {
326
+ await pool.query('DROP TABLE IF EXISTS "states"');
327
+ }
328
+ });
329
+ migrations.push({
330
+ name: '00000000000112_schema-districts',
331
+ up: async ({ context: pool }) => {
332
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
333
+ await pool.query(`
334
+ CREATE TABLE IF NOT EXISTS districts (
335
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
336
+ ${orgColumnDef}
337
+ "name" VARCHAR NOT NULL,
338
+ "state_id" INTEGER NOT NULL,
339
+ "_created" TIMESTAMPTZ NOT NULL,
340
+ "_createdBy" INTEGER NOT NULL,
341
+ "_updated" TIMESTAMPTZ NOT NULL,
342
+ "_updatedBy" INTEGER NOT NULL,
343
+ "_deleted" TIMESTAMPTZ,
344
+ "_deletedBy" INTEGER,
345
+ CONSTRAINT fk_districts_state_id FOREIGN KEY ("state_id") REFERENCES states("_id") ON DELETE CASCADE
346
+ )
347
+ `);
348
+ },
349
+ down: async ({ context: pool }) => {
350
+ await pool.query('DROP TABLE IF EXISTS "districts"');
351
+ }
352
+ });
353
+ migrations.push({
354
+ name: '00000000000113_schema-schools',
355
+ up: async ({ context: pool }) => {
356
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
357
+ await pool.query(`
358
+ CREATE TABLE IF NOT EXISTS schools (
359
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
360
+ ${orgColumnDef}
361
+ "name" VARCHAR NOT NULL,
362
+ "district_id" INTEGER NOT NULL,
363
+ "_created" TIMESTAMPTZ NOT NULL,
364
+ "_createdBy" INTEGER NOT NULL,
365
+ "_updated" TIMESTAMPTZ NOT NULL,
366
+ "_updatedBy" INTEGER NOT NULL,
367
+ "_deleted" TIMESTAMPTZ,
368
+ "_deletedBy" INTEGER,
369
+ CONSTRAINT fk_schools_district_id FOREIGN KEY ("district_id") REFERENCES districts("_id") ON DELETE CASCADE
370
+ )
371
+ `);
372
+ },
373
+ down: async ({ context: pool }) => {
374
+ await pool.query('DROP TABLE IF EXISTS "schools"');
375
+ }
376
+ });
377
+ migrations.push({
378
+ name: '00000000000114_schema-person-schools',
379
+ up: async ({ context: pool }) => {
380
+ const orgColumnDef = isMultiTenant ? '"_orgId" INTEGER,' : '';
381
+ await pool.query(`
382
+ CREATE TABLE IF NOT EXISTS persons_schools (
383
+ "_id" INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
384
+ ${orgColumnDef}
385
+ "person_id" INTEGER NOT NULL,
386
+ "school_id" INTEGER NOT NULL,
387
+ "_created" TIMESTAMPTZ NOT NULL,
388
+ "_createdBy" INTEGER NOT NULL,
389
+ "_updated" TIMESTAMPTZ NOT NULL,
390
+ "_updatedBy" INTEGER NOT NULL,
391
+ "_deleted" TIMESTAMPTZ,
392
+ "_deletedBy" INTEGER,
393
+ CONSTRAINT fk_persons_schools_person_id FOREIGN KEY ("person_id") REFERENCES persons("_id") ON DELETE CASCADE,
394
+ CONSTRAINT fk_persons_schools_school_id FOREIGN KEY ("school_id") REFERENCES schools("_id") ON DELETE CASCADE,
395
+ CONSTRAINT uk_persons_schools_person_school UNIQUE ("person_id", "school_id")
396
+ )
397
+ `);
398
+ },
399
+ down: async ({ context: pool }) => {
400
+ await pool.query('DROP TABLE IF EXISTS "persons_schools"');
401
+ }
402
+ });
282
403
  return migrations;
283
404
  };
@@ -1,6 +1,7 @@
1
1
  import { Join } from '../../operations/join.operation.js';
2
2
  import { JoinMany } from '../../operations/join-many.operation.js';
3
3
  import { JoinThrough } from '../../operations/join-through.operation.js';
4
+ import { JoinThroughMany } from '../../operations/join-through-many.operation.js';
4
5
  export function convertOperationsToPipeline(operations) {
5
6
  let pipeline = [];
6
7
  const processedOperations = [];
@@ -124,6 +125,193 @@ export function convertOperationsToPipeline(operations) {
124
125
  }
125
126
  }
126
127
  else if (operation instanceof JoinThrough) {
128
+ const needsObjectIdConversion = operation.foreignField === '_id';
129
+ const isNestedField = operation.localField.includes('.');
130
+ if (needsObjectIdConversion || isNestedField) {
131
+ pipeline.push({
132
+ $lookup: {
133
+ from: operation.through,
134
+ let: { localId: { $cond: [
135
+ { $eq: [{ $type: `$${operation.localField}` }, 'string'] },
136
+ { $toObjectId: `$${operation.localField}` },
137
+ `$${operation.localField}`
138
+ ] } },
139
+ pipeline: [
140
+ {
141
+ $match: {
142
+ $expr: {
143
+ $eq: [
144
+ {
145
+ $cond: [
146
+ { $eq: [{ $type: `$${operation.throughLocalField}` }, 'string'] },
147
+ { $toObjectId: `$${operation.throughLocalField}` },
148
+ `$${operation.throughLocalField}`
149
+ ]
150
+ },
151
+ '$$localId'
152
+ ]
153
+ }
154
+ }
155
+ },
156
+ { $limit: 1 }
157
+ ],
158
+ as: `${operation.as}_through`
159
+ }
160
+ }, {
161
+ $unwind: {
162
+ path: `$${operation.as}_through`,
163
+ preserveNullAndEmptyArrays: true
164
+ }
165
+ }, {
166
+ $lookup: {
167
+ from: operation.from,
168
+ let: { foreignId: { $cond: [
169
+ { $eq: [{ $type: `$${operation.as}_through.${operation.throughForeignField}` }, 'string'] },
170
+ { $toObjectId: `$${operation.as}_through.${operation.throughForeignField}` },
171
+ `$${operation.as}_through.${operation.throughForeignField}`
172
+ ] } },
173
+ pipeline: [
174
+ {
175
+ $match: {
176
+ $expr: {
177
+ $eq: [
178
+ {
179
+ $cond: [
180
+ { $eq: [{ $type: `$${operation.foreignField}` }, 'string'] },
181
+ { $toObjectId: `$${operation.foreignField}` },
182
+ `$${operation.foreignField}`
183
+ ]
184
+ },
185
+ '$$foreignId'
186
+ ]
187
+ }
188
+ }
189
+ }
190
+ ],
191
+ as: `${operation.as}_temp`
192
+ }
193
+ }, {
194
+ $unwind: {
195
+ path: `$${operation.as}_temp`,
196
+ preserveNullAndEmptyArrays: true
197
+ }
198
+ }, {
199
+ $addFields: {
200
+ [operation.as]: `$${operation.as}_temp`
201
+ }
202
+ }, {
203
+ $project: {
204
+ [`${operation.as}_through`]: 0,
205
+ [`${operation.as}_temp`]: 0
206
+ }
207
+ });
208
+ if (isNestedField) {
209
+ const [parentAlias] = operation.localField.split('.');
210
+ const parentJoin = processedOperations.find(op => (op instanceof Join || op instanceof JoinThrough) && op.as === parentAlias);
211
+ if (parentJoin) {
212
+ pipeline.push({
213
+ $set: {
214
+ [parentAlias]: {
215
+ $mergeObjects: [
216
+ { $ifNull: [`$${parentAlias}`, {}] },
217
+ { [operation.as]: `$${operation.as}` }
218
+ ]
219
+ }
220
+ }
221
+ });
222
+ pipeline.push({
223
+ $unset: operation.as
224
+ });
225
+ }
226
+ }
227
+ }
228
+ else {
229
+ const isNestedFieldElse = operation.localField.includes('.');
230
+ if (isNestedFieldElse) {
231
+ pipeline.push({
232
+ $lookup: {
233
+ from: operation.through,
234
+ let: { localId: `$${operation.localField}` },
235
+ pipeline: [
236
+ {
237
+ $match: {
238
+ $expr: {
239
+ $eq: [`$${operation.throughLocalField}`, '$$localId']
240
+ }
241
+ }
242
+ },
243
+ { $limit: 1 }
244
+ ],
245
+ as: `${operation.as}_through`
246
+ }
247
+ }, {
248
+ $unwind: {
249
+ path: `$${operation.as}_through`,
250
+ preserveNullAndEmptyArrays: true
251
+ }
252
+ });
253
+ }
254
+ else {
255
+ pipeline.push({
256
+ $lookup: {
257
+ from: operation.through,
258
+ localField: operation.localField,
259
+ foreignField: operation.throughLocalField,
260
+ as: `${operation.as}_through`
261
+ }
262
+ }, {
263
+ $unwind: {
264
+ path: `$${operation.as}_through`,
265
+ preserveNullAndEmptyArrays: true
266
+ }
267
+ });
268
+ }
269
+ pipeline.push({
270
+ $lookup: {
271
+ from: operation.from,
272
+ localField: `${operation.as}_through.${operation.throughForeignField}`,
273
+ foreignField: operation.foreignField,
274
+ as: `${operation.as}_temp`
275
+ }
276
+ }, {
277
+ $unwind: {
278
+ path: `$${operation.as}_temp`,
279
+ preserveNullAndEmptyArrays: true
280
+ }
281
+ }, {
282
+ $addFields: {
283
+ [operation.as]: `$${operation.as}_temp`
284
+ }
285
+ }, {
286
+ $project: {
287
+ [`${operation.as}_through`]: 0,
288
+ [`${operation.as}_temp`]: 0
289
+ }
290
+ });
291
+ if (isNestedFieldElse) {
292
+ const [parentAlias] = operation.localField.split('.');
293
+ const parentJoin = processedOperations.find(op => (op instanceof Join || op instanceof JoinThrough) && op.as === parentAlias);
294
+ if (parentJoin) {
295
+ pipeline.push({
296
+ $addFields: {
297
+ [parentAlias]: {
298
+ $mergeObjects: [
299
+ `$${parentAlias}`,
300
+ { [operation.as]: `$${operation.as}` }
301
+ ]
302
+ }
303
+ }
304
+ });
305
+ pipeline.push({
306
+ $project: {
307
+ [operation.as]: 0
308
+ }
309
+ });
310
+ }
311
+ }
312
+ }
313
+ }
314
+ else if (operation instanceof JoinThroughMany) {
127
315
  const needsObjectIdConversion = operation.foreignField === '_id';
128
316
  const isNestedField = operation.localField.includes('.');
129
317
  if (needsObjectIdConversion || isNestedField) {
@@ -208,7 +396,7 @@ export function convertOperationsToPipeline(operations) {
208
396
  });
209
397
  if (isNestedField) {
210
398
  const [parentAlias] = operation.localField.split('.');
211
- const parentJoin = processedOperations.find(op => op instanceof Join && op.as === parentAlias);
399
+ const parentJoin = processedOperations.find(op => (op instanceof Join || op instanceof JoinThrough) && op.as === parentAlias);
212
400
  if (parentJoin) {
213
401
  pipeline.push({
214
402
  $set: {
@@ -293,7 +481,7 @@ export function convertOperationsToPipeline(operations) {
293
481
  });
294
482
  if (isNestedFieldElse) {
295
483
  const [parentAlias] = operation.localField.split('.');
296
- const parentJoin = processedOperations.find(op => op instanceof Join && op.as === parentAlias);
484
+ const parentJoin = processedOperations.find(op => (op instanceof Join || op instanceof JoinThrough) && op.as === parentAlias);
297
485
  if (parentJoin) {
298
486
  pipeline.push({
299
487
  $addFields: {
@@ -0,0 +1,25 @@
1
+ import { IAuditable, IEntity } from "@loomcore/common/models";
2
+ import { IPersonModel } from "./person.model.js";
3
+ export interface IAgentModel extends IEntity, IAuditable {
4
+ person_id: number;
5
+ agent_person?: IPersonModel;
6
+ }
7
+ export declare const agentSchema: import("@sinclair/typebox").TObject<{
8
+ person_id: import("@sinclair/typebox").TNumber;
9
+ agent_person: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
10
+ first_name: import("@sinclair/typebox").TString;
11
+ middle_name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
12
+ last_name: import("@sinclair/typebox").TString;
13
+ phone_numbers: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
14
+ phone_number: import("@sinclair/typebox").TString;
15
+ phone_number_type: import("@sinclair/typebox").TString;
16
+ is_default: import("@sinclair/typebox").TBoolean;
17
+ }>>;
18
+ email_addresses: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
19
+ person_id: import("@sinclair/typebox").TNumber;
20
+ email_address: import("@sinclair/typebox").TString;
21
+ is_default: import("@sinclair/typebox").TBoolean;
22
+ }>>;
23
+ }>>;
24
+ }>;
25
+ export declare const agentModelSpec: import("@loomcore/common/models").IModelSpec<import("@sinclair/typebox").TSchema>;
@@ -0,0 +1,8 @@
1
+ import { entityUtils } from "@loomcore/common/utils";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { personSchema } from "./person.model.js";
4
+ export const agentSchema = Type.Object({
5
+ person_id: Type.Number(),
6
+ agent_person: Type.Optional(personSchema),
7
+ });
8
+ export const agentModelSpec = entityUtils.getModelSpec(agentSchema, { isAuditable: true });
@@ -1,10 +1,12 @@
1
1
  import { IPersonModel } from "./person.model.js";
2
+ import { IAgentModel } from "./agent.model.js";
2
3
  import type { IAuditable, IEntity } from "@loomcore/common/models";
3
4
  export interface IClientReportsModel extends IEntity, IAuditable {
4
- person: IPersonModel;
5
+ client_person: IPersonModel;
6
+ agent?: IAgentModel;
5
7
  }
6
8
  export declare const clientReportsSchema: import("@sinclair/typebox").TObject<{
7
- person: import("@sinclair/typebox").TObject<{
9
+ client_person: import("@sinclair/typebox").TObject<{
8
10
  first_name: import("@sinclair/typebox").TString;
9
11
  middle_name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
10
12
  last_name: import("@sinclair/typebox").TString;
@@ -19,5 +21,23 @@ export declare const clientReportsSchema: import("@sinclair/typebox").TObject<{
19
21
  is_default: import("@sinclair/typebox").TBoolean;
20
22
  }>>;
21
23
  }>;
24
+ agent: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
25
+ person_id: import("@sinclair/typebox").TNumber;
26
+ agent_person: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
27
+ first_name: import("@sinclair/typebox").TString;
28
+ middle_name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
29
+ last_name: import("@sinclair/typebox").TString;
30
+ phone_numbers: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
31
+ phone_number: import("@sinclair/typebox").TString;
32
+ phone_number_type: import("@sinclair/typebox").TString;
33
+ is_default: import("@sinclair/typebox").TBoolean;
34
+ }>>;
35
+ email_addresses: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
36
+ person_id: import("@sinclair/typebox").TNumber;
37
+ email_address: import("@sinclair/typebox").TString;
38
+ is_default: import("@sinclair/typebox").TBoolean;
39
+ }>>;
40
+ }>>;
41
+ }>>;
22
42
  }>;
23
43
  export declare const clientReportsModelSpec: import("@loomcore/common/models").IModelSpec<import("@sinclair/typebox").TSchema>;
@@ -1,7 +1,9 @@
1
1
  import { personSchema } from "./person.model.js";
2
+ import { agentSchema } from "./agent.model.js";
2
3
  import { entityUtils } from "@loomcore/common/utils";
3
4
  import { Type } from "@sinclair/typebox";
4
5
  export const clientReportsSchema = Type.Object({
5
- person: personSchema
6
+ client_person: personSchema,
7
+ agent: Type.Optional(agentSchema)
6
8
  });
7
9
  export const clientReportsModelSpec = entityUtils.getModelSpec(clientReportsSchema, { isAuditable: true });
@@ -0,0 +1,12 @@
1
+ import type { IAuditable, IEntity } from "@loomcore/common/models";
2
+ import { IStateModel } from "./state.model.js";
3
+ export interface IDistrictModel extends IEntity, IAuditable {
4
+ name: string;
5
+ state_id: number;
6
+ state?: IStateModel;
7
+ }
8
+ export declare const districtSchema: import("@sinclair/typebox").TObject<{
9
+ name: import("@sinclair/typebox").TString;
10
+ state_id: import("@sinclair/typebox").TNumber;
11
+ }>;
12
+ export declare const districtModelSpec: import("@loomcore/common/models").IModelSpec<import("@sinclair/typebox").TSchema>;
@@ -0,0 +1,7 @@
1
+ import { entityUtils } from "@loomcore/common/utils";
2
+ import { Type } from "@sinclair/typebox";
3
+ export const districtSchema = Type.Object({
4
+ name: Type.String(),
5
+ state_id: Type.Number(),
6
+ });
7
+ export const districtModelSpec = entityUtils.getModelSpec(districtSchema, { isAuditable: true });
@@ -1,12 +1,14 @@
1
1
  import type { IAuditable, IEntity } from "@loomcore/common/models";
2
2
  import { IEmailAddressModel } from "./email-address.model.js";
3
3
  import { IPhoneNumberModel } from "./phone-number.model.js";
4
+ import { ISchoolModel } from "./school.model.js";
4
5
  export interface IPersonModel extends IEntity, IAuditable {
5
6
  first_name: string;
6
7
  middle_name: string | null;
7
8
  last_name: string;
8
9
  email_addresses: IEmailAddressModel[];
9
10
  phone_numbers: IPhoneNumberModel[];
11
+ school?: ISchoolModel;
10
12
  }
11
13
  export declare const personSchema: import("@sinclair/typebox").TObject<{
12
14
  first_name: import("@sinclair/typebox").TString;
@@ -0,0 +1,12 @@
1
+ import type { IAuditable, IEntity } from "@loomcore/common/models";
2
+ import { IDistrictModel } from "./district.model.js";
3
+ export interface ISchoolModel extends IEntity, IAuditable {
4
+ name: string;
5
+ district_id: number;
6
+ district?: IDistrictModel;
7
+ }
8
+ export declare const schoolSchema: import("@sinclair/typebox").TObject<{
9
+ name: import("@sinclair/typebox").TString;
10
+ district_id: import("@sinclair/typebox").TNumber;
11
+ }>;
12
+ export declare const schoolModelSpec: import("@loomcore/common/models").IModelSpec<import("@sinclair/typebox").TSchema>;
@@ -0,0 +1,7 @@
1
+ import { entityUtils } from "@loomcore/common/utils";
2
+ import { Type } from "@sinclair/typebox";
3
+ export const schoolSchema = Type.Object({
4
+ name: Type.String(),
5
+ district_id: Type.Number(),
6
+ });
7
+ export const schoolModelSpec = entityUtils.getModelSpec(schoolSchema, { isAuditable: true });
@@ -0,0 +1,8 @@
1
+ import type { IAuditable, IEntity } from "@loomcore/common/models";
2
+ export interface IStateModel extends IEntity, IAuditable {
3
+ name: string;
4
+ }
5
+ export declare const stateSchema: import("@sinclair/typebox").TObject<{
6
+ name: import("@sinclair/typebox").TString;
7
+ }>;
8
+ export declare const stateModelSpec: import("@loomcore/common/models").IModelSpec<import("@sinclair/typebox").TSchema>;
@@ -0,0 +1,6 @@
1
+ import { entityUtils } from "@loomcore/common/utils";
2
+ import { Type } from "@sinclair/typebox";
3
+ export const stateSchema = Type.Object({
4
+ name: Type.String(),
5
+ });
6
+ export const stateModelSpec = entityUtils.getModelSpec(stateSchema, { isAuditable: true });
@@ -2,3 +2,4 @@ export * from './operation.js';
2
2
  export * from './join.operation.js';
3
3
  export * from './join-many.operation.js';
4
4
  export * from './join-through.operation.js';
5
+ export * from './join-through-many.operation.js';
@@ -2,3 +2,4 @@ export * from './operation.js';
2
2
  export * from './join.operation.js';
3
3
  export * from './join-many.operation.js';
4
4
  export * from './join-through.operation.js';
5
+ export * from './join-through-many.operation.js';
@@ -0,0 +1,10 @@
1
+ export declare class JoinThroughMany {
2
+ from: string;
3
+ through: string;
4
+ localField: string;
5
+ throughLocalField: string;
6
+ throughForeignField: string;
7
+ foreignField: string;
8
+ as: string;
9
+ constructor(from: string, through: string, localField: string, throughLocalField: string, throughForeignField: string, foreignField: string, as: string);
10
+ }
@@ -0,0 +1,18 @@
1
+ export class JoinThroughMany {
2
+ from;
3
+ through;
4
+ localField;
5
+ throughLocalField;
6
+ throughForeignField;
7
+ foreignField;
8
+ as;
9
+ constructor(from, through, localField, throughLocalField, throughForeignField, foreignField, as) {
10
+ this.from = from;
11
+ this.through = through;
12
+ this.localField = localField;
13
+ this.throughLocalField = throughLocalField;
14
+ this.throughForeignField = throughForeignField;
15
+ this.foreignField = foreignField;
16
+ this.as = as;
17
+ }
18
+ }
@@ -1,4 +1,5 @@
1
1
  import { Join } from "./join.operation.js";
2
2
  import { JoinMany } from "./join-many.operation.js";
3
3
  import { JoinThrough } from "./join-through.operation.js";
4
- export type Operation = Join | JoinMany | JoinThrough;
4
+ import { JoinThroughMany } from "./join-through-many.operation.js";
5
+ export type Operation = Join | JoinMany | JoinThrough | JoinThroughMany;
@@ -1,6 +1,7 @@
1
1
  import { Join } from "../../operations/join.operation.js";
2
2
  import { JoinMany } from "../../operations/join-many.operation.js";
3
3
  import { JoinThrough } from "../../operations/join-through.operation.js";
4
+ import { JoinThroughMany } from "../../operations/join-through-many.operation.js";
4
5
  import { BadRequestError } from "../../../errors/index.js";
5
6
  import { buildJoinClauses } from '../utils/build-join-clauses.js';
6
7
  import { buildSelectClause } from '../utils/build-select-clause.js';
@@ -39,7 +40,7 @@ export async function batchUpdate(client, entities, operations, queryObject, plu
39
40
  }
40
41
  await client.query('COMMIT');
41
42
  const joinClauses = buildJoinClauses(operations, pluralResourceName);
42
- const hasJoins = operations.some(op => op instanceof Join || op instanceof JoinMany || op instanceof JoinThrough);
43
+ const hasJoins = operations.some(op => op instanceof Join || op instanceof JoinMany || op instanceof JoinThrough || op instanceof JoinThroughMany);
43
44
  const tablePrefix = hasJoins ? pluralResourceName : undefined;
44
45
  queryObject.filters._id = { in: entityIds };
45
46
  const { whereClause, values } = buildWhereClause(queryObject, [], tablePrefix);
@@ -1,12 +1,13 @@
1
1
  import { Join } from "../../operations/join.operation.js";
2
2
  import { JoinMany } from "../../operations/join-many.operation.js";
3
3
  import { JoinThrough } from "../../operations/join-through.operation.js";
4
+ import { JoinThroughMany } from "../../operations/join-through-many.operation.js";
4
5
  import { buildJoinClauses } from '../utils/build-join-clauses.js';
5
6
  import { buildSelectClause } from '../utils/build-select-clause.js';
6
7
  import { transformJoinResults } from '../utils/transform-join-results.js';
7
8
  export async function getAll(client, operations, pluralResourceName) {
8
9
  const joinClauses = buildJoinClauses(operations, pluralResourceName);
9
- const hasJoins = operations.some(op => op instanceof Join || op instanceof JoinMany || op instanceof JoinThrough);
10
+ const hasJoins = operations.some(op => op instanceof Join || op instanceof JoinMany || op instanceof JoinThrough || op instanceof JoinThroughMany);
10
11
  const selectClause = hasJoins
11
12
  ? await buildSelectClause(client, pluralResourceName, pluralResourceName, operations)
12
13
  : '*';
@@ -1,6 +1,7 @@
1
1
  import { Join } from "../../operations/join.operation.js";
2
2
  import { JoinMany } from "../../operations/join-many.operation.js";
3
3
  import { JoinThrough } from "../../operations/join-through.operation.js";
4
+ import { JoinThroughMany } from "../../operations/join-through-many.operation.js";
4
5
  import { buildJoinClauses } from "../utils/build-join-clauses.js";
5
6
  import { buildSelectClause } from "../utils/build-select-clause.js";
6
7
  import { transformJoinResults } from "../utils/transform-join-results.js";
@@ -10,7 +11,7 @@ export async function getById(client, operations, queryObject, id, pluralResourc
10
11
  if (!id)
11
12
  throw new BadRequestError('id is required');
12
13
  const joinClauses = buildJoinClauses(operations, pluralResourceName);
13
- const hasJoins = operations.some(op => op instanceof Join || op instanceof JoinMany || op instanceof JoinThrough);
14
+ const hasJoins = operations.some(op => op instanceof Join || op instanceof JoinMany || op instanceof JoinThrough || op instanceof JoinThroughMany);
14
15
  const selectClause = hasJoins
15
16
  ? await buildSelectClause(client, pluralResourceName, pluralResourceName, operations)
16
17
  : '*';
@@ -1,6 +1,7 @@
1
1
  import { Join } from "../../operations/join.operation.js";
2
2
  import { JoinMany } from "../../operations/join-many.operation.js";
3
3
  import { JoinThrough } from "../../operations/join-through.operation.js";
4
+ import { JoinThroughMany } from "../../operations/join-through-many.operation.js";
4
5
  import { buildWhereClause } from "../utils/build-where-clause.js";
5
6
  import { buildOrderByClause } from "../utils/build-order-by-clause.js";
6
7
  import { buildJoinClauses } from "../utils/build-join-clauses.js";
@@ -13,7 +14,7 @@ export async function get(client, operations, queryOptions, pluralResourceName)
13
14
  const joinClauses = buildJoinClauses(operations, pluralResourceName);
14
15
  const orderByClause = buildOrderByClause(queryOptions);
15
16
  const paginationClause = buildPaginationClause(queryOptions);
16
- const hasJoins = operations.some(op => op instanceof Join || op instanceof JoinMany || op instanceof JoinThrough);
17
+ const hasJoins = operations.some(op => op instanceof Join || op instanceof JoinMany || op instanceof JoinThrough || op instanceof JoinThroughMany);
17
18
  const selectClause = hasJoins
18
19
  ? await buildSelectClause(client, pluralResourceName, pluralResourceName, operations)
19
20
  : '*';
@@ -1,55 +1,91 @@
1
1
  import { Join } from "../../operations/join.operation.js";
2
2
  import { JoinMany } from "../../operations/join-many.operation.js";
3
3
  import { JoinThrough } from "../../operations/join-through.operation.js";
4
+ import { JoinThroughMany } from "../../operations/join-through-many.operation.js";
4
5
  export function buildJoinClauses(operations, mainTableName) {
5
6
  let joinClauses = '';
6
- const joinOperations = operations.filter(op => op instanceof Join);
7
- const joinManyOperations = operations.filter(op => op instanceof JoinMany);
8
7
  const joinThroughOperations = operations.filter(op => op instanceof JoinThrough);
9
- for (const operation of joinOperations) {
10
- const localFieldRef = mainTableName
11
- ? `"${mainTableName}"."${operation.localField}"`
12
- : `"${operation.localField}"`;
13
- joinClauses += ` LEFT JOIN "${operation.from}" AS ${operation.as} ON ${localFieldRef} = "${operation.as}"."${operation.foreignField}"`;
14
- }
15
- for (const joinMany of joinManyOperations) {
16
- let localFieldRef;
17
- if (joinMany.localField.includes('.')) {
18
- const [tableAlias, columnName] = joinMany.localField.split('.');
19
- localFieldRef = `${tableAlias}."${columnName}"`;
8
+ for (const operation of operations) {
9
+ if (operation instanceof Join) {
10
+ let localFieldRef;
11
+ if (operation.localField.includes('.')) {
12
+ const [tableAlias, columnName] = operation.localField.split('.');
13
+ const referencedJoinThrough = joinThroughOperations.find(jt => jt.as === tableAlias);
14
+ if (referencedJoinThrough) {
15
+ localFieldRef = `(${tableAlias}.aggregated->>'${columnName}')::integer`;
16
+ }
17
+ else {
18
+ localFieldRef = `${tableAlias}."${columnName}"`;
19
+ }
20
+ }
21
+ else {
22
+ localFieldRef = mainTableName
23
+ ? `"${mainTableName}"."${operation.localField}"`
24
+ : `"${operation.localField}"`;
25
+ }
26
+ joinClauses += ` LEFT JOIN "${operation.from}" AS ${operation.as} ON ${localFieldRef} = "${operation.as}"."${operation.foreignField}"`;
20
27
  }
21
- else {
22
- localFieldRef = mainTableName
23
- ? `"${mainTableName}"."${joinMany.localField}"`
24
- : `"${joinMany.localField}"`;
28
+ else if (operation instanceof JoinMany) {
29
+ let localFieldRef;
30
+ if (operation.localField.includes('.')) {
31
+ const [tableAlias, columnName] = operation.localField.split('.');
32
+ localFieldRef = `${tableAlias}."${columnName}"`;
33
+ }
34
+ else {
35
+ localFieldRef = mainTableName
36
+ ? `"${mainTableName}"."${operation.localField}"`
37
+ : `"${operation.localField}"`;
38
+ }
39
+ joinClauses += ` LEFT JOIN LATERAL (
40
+ SELECT COALESCE(JSON_AGG(row_to_json("${operation.from}")), '[]'::json) AS aggregated
41
+ FROM "${operation.from}"
42
+ WHERE "${operation.from}"."${operation.foreignField}" = ${localFieldRef}
43
+ AND "${operation.from}"."_deleted" IS NULL
44
+ ) AS ${operation.as} ON true`;
25
45
  }
26
- joinClauses += ` LEFT JOIN LATERAL (
27
- SELECT COALESCE(JSON_AGG(row_to_json("${joinMany.from}")), '[]'::json) AS aggregated
28
- FROM "${joinMany.from}"
29
- WHERE "${joinMany.from}"."${joinMany.foreignField}" = ${localFieldRef}
30
- AND "${joinMany.from}"."_deleted" IS NULL
31
- ) AS ${joinMany.as} ON true`;
32
- }
33
- for (const joinThrough of joinThroughOperations) {
34
- let localFieldRef;
35
- if (joinThrough.localField.includes('.')) {
36
- const [tableAlias, columnName] = joinThrough.localField.split('.');
37
- localFieldRef = `${tableAlias}."${columnName}"`;
46
+ else if (operation instanceof JoinThrough) {
47
+ let localFieldRef;
48
+ if (operation.localField.includes('.')) {
49
+ const [tableAlias, columnName] = operation.localField.split('.');
50
+ localFieldRef = `${tableAlias}."${columnName}"`;
51
+ }
52
+ else {
53
+ localFieldRef = mainTableName
54
+ ? `"${mainTableName}"."${operation.localField}"`
55
+ : `"${operation.localField}"`;
56
+ }
57
+ joinClauses += ` LEFT JOIN LATERAL (
58
+ SELECT row_to_json(${operation.as}) AS aggregated
59
+ FROM "${operation.through}"
60
+ INNER JOIN "${operation.from}" AS ${operation.as}
61
+ ON ${operation.as}."${operation.foreignField}" = "${operation.through}"."${operation.throughForeignField}"
62
+ WHERE "${operation.through}"."${operation.throughLocalField}" = ${localFieldRef}
63
+ AND "${operation.through}"."_deleted" IS NULL
64
+ AND ${operation.as}."_deleted" IS NULL
65
+ LIMIT 1
66
+ ) AS ${operation.as} ON true`;
38
67
  }
39
- else {
40
- localFieldRef = mainTableName
41
- ? `"${mainTableName}"."${joinThrough.localField}"`
42
- : `"${joinThrough.localField}"`;
68
+ else if (operation instanceof JoinThroughMany) {
69
+ let localFieldRef;
70
+ if (operation.localField.includes('.')) {
71
+ const [tableAlias, columnName] = operation.localField.split('.');
72
+ localFieldRef = `${tableAlias}."${columnName}"`;
73
+ }
74
+ else {
75
+ localFieldRef = mainTableName
76
+ ? `"${mainTableName}"."${operation.localField}"`
77
+ : `"${operation.localField}"`;
78
+ }
79
+ joinClauses += ` LEFT JOIN LATERAL (
80
+ SELECT COALESCE(JSON_AGG(row_to_json(${operation.as})), '[]'::json) AS aggregated
81
+ FROM "${operation.through}"
82
+ INNER JOIN "${operation.from}" AS ${operation.as}
83
+ ON ${operation.as}."${operation.foreignField}" = "${operation.through}"."${operation.throughForeignField}"
84
+ WHERE "${operation.through}"."${operation.throughLocalField}" = ${localFieldRef}
85
+ AND "${operation.through}"."_deleted" IS NULL
86
+ AND ${operation.as}."_deleted" IS NULL
87
+ ) AS ${operation.as} ON true`;
43
88
  }
44
- joinClauses += ` LEFT JOIN LATERAL (
45
- SELECT COALESCE(JSON_AGG(row_to_json(${joinThrough.as})), '[]'::json) AS aggregated
46
- FROM "${joinThrough.through}"
47
- INNER JOIN "${joinThrough.from}" AS ${joinThrough.as}
48
- ON ${joinThrough.as}."${joinThrough.foreignField}" = "${joinThrough.through}"."${joinThrough.throughForeignField}"
49
- WHERE "${joinThrough.through}"."${joinThrough.throughLocalField}" = ${localFieldRef}
50
- AND "${joinThrough.through}"."_deleted" IS NULL
51
- AND ${joinThrough.as}."_deleted" IS NULL
52
- ) AS ${joinThrough.as} ON true`;
53
89
  }
54
90
  return joinClauses;
55
91
  }
@@ -1,6 +1,7 @@
1
1
  import { Join } from '../../operations/join.operation.js';
2
2
  import { JoinMany } from '../../operations/join-many.operation.js';
3
3
  import { JoinThrough } from '../../operations/join-through.operation.js';
4
+ import { JoinThroughMany } from '../../operations/join-through-many.operation.js';
4
5
  async function getTableColumns(client, tableName) {
5
6
  const result = await client.query(`
6
7
  SELECT column_name
@@ -15,6 +16,7 @@ export async function buildSelectClause(client, mainTableName, mainTableAlias, o
15
16
  const joinOperations = operations.filter(op => op instanceof Join);
16
17
  const joinManyOperations = operations.filter(op => op instanceof JoinMany);
17
18
  const joinThroughOperations = operations.filter(op => op instanceof JoinThrough);
19
+ const joinThroughManyOperations = operations.filter(op => op instanceof JoinThroughMany);
18
20
  const mainTableColumns = await getTableColumns(client, mainTableName);
19
21
  const mainSelects = mainTableColumns.map(col => `"${mainTableName}"."${col}" AS "${col}"`);
20
22
  const joinSelects = [];
@@ -33,6 +35,9 @@ export async function buildSelectClause(client, mainTableName, mainTableAlias, o
33
35
  for (const joinThrough of joinThroughOperations) {
34
36
  joinSelects.push(`${joinThrough.as}.aggregated AS "${joinThrough.as}"`);
35
37
  }
38
+ for (const joinThroughMany of joinThroughManyOperations) {
39
+ joinSelects.push(`${joinThroughMany.as}.aggregated AS "${joinThroughMany.as}"`);
40
+ }
36
41
  const allSelects = [...mainSelects, ...joinSelects];
37
42
  return allSelects.join(', ');
38
43
  }
@@ -1,11 +1,13 @@
1
1
  import { Join } from '../../operations/join.operation.js';
2
2
  import { JoinMany } from '../../operations/join-many.operation.js';
3
3
  import { JoinThrough } from '../../operations/join-through.operation.js';
4
+ import { JoinThroughMany } from '../../operations/join-through-many.operation.js';
4
5
  export function transformJoinResults(rows, operations) {
5
6
  const joinOperations = operations.filter(op => op instanceof Join);
6
7
  const joinManyOperations = operations.filter(op => op instanceof JoinMany);
7
8
  const joinThroughOperations = operations.filter(op => op instanceof JoinThrough);
8
- if (joinOperations.length === 0 && joinManyOperations.length === 0 && joinThroughOperations.length === 0) {
9
+ const joinThroughManyOperations = operations.filter(op => op instanceof JoinThroughMany);
10
+ if (joinOperations.length === 0 && joinManyOperations.length === 0 && joinThroughOperations.length === 0 && joinThroughManyOperations.length === 0) {
9
11
  return rows;
10
12
  }
11
13
  return rows.map(row => {
@@ -13,7 +15,8 @@ export function transformJoinResults(rows, operations) {
13
15
  const allJoinAliases = [
14
16
  ...joinOperations.map(j => j.as),
15
17
  ...joinManyOperations.map(j => j.as),
16
- ...joinThroughOperations.map(j => j.as)
18
+ ...joinThroughOperations.map(j => j.as),
19
+ ...joinThroughManyOperations.map(j => j.as)
17
20
  ];
18
21
  for (const key of Object.keys(row)) {
19
22
  const hasJoinPrefix = joinOperations.some(join => key.startsWith(`${join.as}__`));
@@ -22,21 +25,100 @@ export function transformJoinResults(rows, operations) {
22
25
  transformed[key] = row[key];
23
26
  }
24
27
  }
25
- for (const join of joinOperations) {
26
- const prefix = `${join.as}__`;
27
- const joinedData = {};
28
- let hasAnyData = false;
29
- for (const key of Object.keys(row)) {
30
- if (key.startsWith(prefix)) {
31
- const columnName = key.substring(prefix.length);
32
- const value = row[key];
33
- joinedData[columnName] = value;
34
- if (value !== null && value !== undefined) {
35
- hasAnyData = true;
28
+ for (const operation of operations) {
29
+ if (operation instanceof JoinThrough) {
30
+ const jsonValue = row[operation.as];
31
+ let parsedValue;
32
+ if (jsonValue !== null && jsonValue !== undefined) {
33
+ parsedValue = typeof jsonValue === 'string'
34
+ ? JSON.parse(jsonValue)
35
+ : jsonValue;
36
+ }
37
+ else {
38
+ parsedValue = null;
39
+ }
40
+ if (operation.localField.includes('.')) {
41
+ const [tableAlias] = operation.localField.split('.');
42
+ const relatedJoin = joinOperations.find(j => j.as === tableAlias);
43
+ const relatedJoinThrough = joinThroughOperations.find(j => j.as === tableAlias);
44
+ if ((relatedJoin && transformed[relatedJoin.as]) || (relatedJoinThrough && transformed[relatedJoinThrough.as])) {
45
+ const targetAlias = relatedJoin ? relatedJoin.as : relatedJoinThrough.as;
46
+ transformed[targetAlias][operation.as] = parsedValue;
47
+ }
48
+ else {
49
+ transformed[operation.as] = parsedValue;
36
50
  }
37
51
  }
52
+ else {
53
+ transformed[operation.as] = parsedValue;
54
+ }
55
+ }
56
+ else if (operation instanceof Join) {
57
+ const prefix = `${operation.as}__`;
58
+ const joinedData = {};
59
+ let hasAnyData = false;
60
+ for (const key of Object.keys(row)) {
61
+ if (key.startsWith(prefix)) {
62
+ const columnName = key.substring(prefix.length);
63
+ const value = row[key];
64
+ joinedData[columnName] = value;
65
+ if (value !== null && value !== undefined) {
66
+ hasAnyData = true;
67
+ }
68
+ }
69
+ }
70
+ if (operation.localField.includes('.')) {
71
+ const [tableAlias] = operation.localField.split('.');
72
+ const relatedJoin = joinOperations.find(j => j.as === tableAlias);
73
+ const relatedJoinThrough = joinThroughOperations.find(j => j.as === tableAlias);
74
+ const findNestedObject = (obj, alias, path = []) => {
75
+ if (obj[alias] !== undefined && obj[alias] !== null) {
76
+ return { obj: obj[alias], path: [...path, alias] };
77
+ }
78
+ for (const key in obj) {
79
+ if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
80
+ const found = findNestedObject(obj[key], alias, [...path, key]);
81
+ if (found !== null) {
82
+ return found;
83
+ }
84
+ }
85
+ }
86
+ return null;
87
+ };
88
+ let targetObject = null;
89
+ if (relatedJoin) {
90
+ if (transformed[relatedJoin.as] !== undefined && transformed[relatedJoin.as] !== null) {
91
+ targetObject = transformed[relatedJoin.as];
92
+ }
93
+ else {
94
+ const found = findNestedObject(transformed, relatedJoin.as);
95
+ if (found !== null && found.obj !== undefined && found.obj !== null) {
96
+ targetObject = found.obj;
97
+ }
98
+ }
99
+ }
100
+ else if (relatedJoinThrough) {
101
+ const found = findNestedObject(transformed, relatedJoinThrough.as);
102
+ if (found !== null && found.obj !== undefined && found.obj !== null) {
103
+ targetObject = found.obj;
104
+ }
105
+ }
106
+ if (targetObject) {
107
+ if (hasAnyData) {
108
+ targetObject[operation.as] = joinedData;
109
+ }
110
+ else {
111
+ targetObject[operation.as] = null;
112
+ }
113
+ }
114
+ else {
115
+ transformed[operation.as] = hasAnyData ? joinedData : null;
116
+ }
117
+ }
118
+ else {
119
+ transformed[operation.as] = hasAnyData ? joinedData : null;
120
+ }
38
121
  }
39
- transformed[join.as] = hasAnyData ? joinedData : null;
40
122
  }
41
123
  for (const joinMany of joinManyOperations) {
42
124
  const jsonValue = row[joinMany.as];
@@ -63,8 +145,8 @@ export function transformJoinResults(rows, operations) {
63
145
  transformed[joinMany.as] = parsedValue;
64
146
  }
65
147
  }
66
- for (const joinThrough of joinThroughOperations) {
67
- const jsonValue = row[joinThrough.as];
148
+ for (const joinThroughMany of joinThroughManyOperations) {
149
+ const jsonValue = row[joinThroughMany.as];
68
150
  let parsedValue;
69
151
  if (jsonValue !== null && jsonValue !== undefined) {
70
152
  parsedValue = typeof jsonValue === 'string'
@@ -74,18 +156,20 @@ export function transformJoinResults(rows, operations) {
74
156
  else {
75
157
  parsedValue = [];
76
158
  }
77
- if (joinThrough.localField.includes('.')) {
78
- const [tableAlias] = joinThrough.localField.split('.');
159
+ if (joinThroughMany.localField.includes('.')) {
160
+ const [tableAlias] = joinThroughMany.localField.split('.');
79
161
  const relatedJoin = joinOperations.find(j => j.as === tableAlias);
80
- if (relatedJoin && transformed[relatedJoin.as]) {
81
- transformed[relatedJoin.as][joinThrough.as] = parsedValue;
162
+ const relatedJoinThrough = joinThroughOperations.find(j => j.as === tableAlias);
163
+ if ((relatedJoin && transformed[relatedJoin.as]) || (relatedJoinThrough && transformed[relatedJoinThrough.as])) {
164
+ const targetAlias = relatedJoin ? relatedJoin.as : relatedJoinThrough.as;
165
+ transformed[targetAlias][joinThroughMany.as] = parsedValue;
82
166
  }
83
167
  else {
84
- transformed[joinThrough.as] = parsedValue;
168
+ transformed[joinThroughMany.as] = parsedValue;
85
169
  }
86
170
  }
87
171
  else {
88
- transformed[joinThrough.as] = parsedValue;
172
+ transformed[joinThroughMany.as] = parsedValue;
89
173
  }
90
174
  }
91
175
  return transformed;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcore/api",
3
- "version": "0.1.70",
3
+ "version": "0.1.73",
4
4
  "private": false,
5
5
  "description": "Loom Core Api - An opinionated Node.js api using Typescript, Express, and MongoDb or PostgreSQL",
6
6
  "scripts": {