@loomcore/api 0.1.69 → 0.1.71
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/__tests__/postgres-test-migrations/postgres-test-schema.js +27 -2
- package/dist/controllers/index.d.ts +1 -0
- package/dist/controllers/index.js +1 -0
- package/dist/controllers/query-api.controller.d.ts +19 -0
- package/dist/controllers/query-api.controller.js +61 -0
- package/dist/databases/operations/__tests__/models/agent.model.d.ts +25 -0
- package/dist/databases/operations/__tests__/models/agent.model.js +8 -0
- package/dist/databases/operations/__tests__/models/client-report.model.d.ts +22 -2
- package/dist/databases/operations/__tests__/models/client-report.model.js +3 -1
- package/dist/databases/postgres/utils/build-join-clauses.js +10 -3
- package/dist/databases/postgres/utils/transform-join-results.js +18 -1
- package/dist/services/generic-query-service/generic-query-service.interface.d.ts +1 -0
- package/dist/services/generic-query-service/generic-query.service.d.ts +1 -0
- package/dist/services/generic-query-service/generic-query.service.js +4 -0
- package/package.json +1 -1
|
@@ -123,7 +123,30 @@ export const getPostgresTestSchema = (config) => {
|
|
|
123
123
|
}
|
|
124
124
|
});
|
|
125
125
|
migrations.push({
|
|
126
|
-
name: '
|
|
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
|
},
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Application, NextFunction, Request, Response } from 'express';
|
|
2
|
+
import { IEntity, IModelSpec } from '@loomcore/common/models';
|
|
3
|
+
import type { TSchema } from '@sinclair/typebox';
|
|
4
|
+
import { IGenericQueryService } from '../services/index.js';
|
|
5
|
+
export declare abstract class QueryApiController<T extends IEntity> {
|
|
6
|
+
protected app: Application;
|
|
7
|
+
protected service: IGenericQueryService<T>;
|
|
8
|
+
protected slug: string;
|
|
9
|
+
protected apiResourceName: string;
|
|
10
|
+
protected modelSpec?: IModelSpec;
|
|
11
|
+
protected publicSpec?: IModelSpec;
|
|
12
|
+
protected idSchema: TSchema;
|
|
13
|
+
protected constructor(slug: string, app: Application, service: IGenericQueryService<T>, resourceName?: string, modelSpec?: IModelSpec, publicSpec?: IModelSpec);
|
|
14
|
+
mapRoutes(app: Application): void;
|
|
15
|
+
getAll(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
16
|
+
get(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
17
|
+
getById(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
18
|
+
getCount(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BadRequestError } from '../errors/index.js';
|
|
2
|
+
import { Value } from '@sinclair/typebox/value';
|
|
3
|
+
import { getIdSchema } from '@loomcore/common/validation';
|
|
4
|
+
import { apiUtils } from '../utils/index.js';
|
|
5
|
+
import { isAuthorized } from '../middleware/index.js';
|
|
6
|
+
export class QueryApiController {
|
|
7
|
+
app;
|
|
8
|
+
service;
|
|
9
|
+
slug;
|
|
10
|
+
apiResourceName;
|
|
11
|
+
modelSpec;
|
|
12
|
+
publicSpec;
|
|
13
|
+
idSchema;
|
|
14
|
+
constructor(slug, app, service, resourceName = '', modelSpec, publicSpec) {
|
|
15
|
+
this.slug = slug;
|
|
16
|
+
this.app = app;
|
|
17
|
+
this.service = service;
|
|
18
|
+
this.apiResourceName = resourceName;
|
|
19
|
+
this.modelSpec = modelSpec;
|
|
20
|
+
this.publicSpec = publicSpec;
|
|
21
|
+
this.idSchema = getIdSchema();
|
|
22
|
+
this.mapRoutes(app);
|
|
23
|
+
}
|
|
24
|
+
mapRoutes(app) {
|
|
25
|
+
app.get(`/api/${this.slug}`, isAuthorized(), this.get.bind(this));
|
|
26
|
+
app.get(`/api/${this.slug}/all`, isAuthorized(), this.getAll.bind(this));
|
|
27
|
+
app.get(`/api/${this.slug}/count`, isAuthorized(), this.getCount.bind(this));
|
|
28
|
+
app.get(`/api/${this.slug}/:id`, isAuthorized(), this.getById.bind(this));
|
|
29
|
+
}
|
|
30
|
+
async getAll(req, res, next) {
|
|
31
|
+
res.set('Content-Type', 'application/json');
|
|
32
|
+
const entities = await this.service.getAll(req.userContext);
|
|
33
|
+
apiUtils.apiResponse(res, 200, { data: entities }, this.modelSpec, this.publicSpec);
|
|
34
|
+
}
|
|
35
|
+
async get(req, res, next) {
|
|
36
|
+
res.set('Content-Type', 'application/json');
|
|
37
|
+
const queryOptions = apiUtils.getQueryOptionsFromRequest(req);
|
|
38
|
+
const pagedResult = await this.service.get(req.userContext, queryOptions);
|
|
39
|
+
apiUtils.apiResponse(res, 200, { data: pagedResult }, this.modelSpec, this.publicSpec);
|
|
40
|
+
}
|
|
41
|
+
async getById(req, res, next) {
|
|
42
|
+
res.set('Content-Type', 'application/json');
|
|
43
|
+
const idParam = req.params?.id;
|
|
44
|
+
if (!idParam) {
|
|
45
|
+
throw new BadRequestError('ID parameter is required');
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const id = Value.Convert(this.idSchema, idParam);
|
|
49
|
+
const entity = await this.service.getById(req.userContext, id);
|
|
50
|
+
apiUtils.apiResponse(res, 200, { data: entity }, this.modelSpec, this.publicSpec);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw new BadRequestError(`Invalid ID format: ${error.message || error}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async getCount(req, res, next) {
|
|
57
|
+
res.set('Content-Type', 'application/json');
|
|
58
|
+
const count = await this.service.getCount(req.userContext);
|
|
59
|
+
apiUtils.apiResponse(res, 200, { data: count }, this.modelSpec, this.publicSpec);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -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
|
-
|
|
5
|
+
client_person: IPersonModel;
|
|
6
|
+
agent?: IAgentModel;
|
|
5
7
|
}
|
|
6
8
|
export declare const clientReportsSchema: import("@sinclair/typebox").TObject<{
|
|
7
|
-
|
|
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
|
-
|
|
6
|
+
client_person: personSchema,
|
|
7
|
+
agent: Type.Optional(agentSchema)
|
|
6
8
|
});
|
|
7
9
|
export const clientReportsModelSpec = entityUtils.getModelSpec(clientReportsSchema, { isAuditable: true });
|
|
@@ -7,9 +7,16 @@ export function buildJoinClauses(operations, mainTableName) {
|
|
|
7
7
|
const joinManyOperations = operations.filter(op => op instanceof JoinMany);
|
|
8
8
|
const joinThroughOperations = operations.filter(op => op instanceof JoinThrough);
|
|
9
9
|
for (const operation of joinOperations) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
let localFieldRef;
|
|
11
|
+
if (operation.localField.includes('.')) {
|
|
12
|
+
const [tableAlias, columnName] = operation.localField.split('.');
|
|
13
|
+
localFieldRef = `${tableAlias}."${columnName}"`;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
localFieldRef = mainTableName
|
|
17
|
+
? `"${mainTableName}"."${operation.localField}"`
|
|
18
|
+
: `"${operation.localField}"`;
|
|
19
|
+
}
|
|
13
20
|
joinClauses += ` LEFT JOIN "${operation.from}" AS ${operation.as} ON ${localFieldRef} = "${operation.as}"."${operation.foreignField}"`;
|
|
14
21
|
}
|
|
15
22
|
for (const joinMany of joinManyOperations) {
|
|
@@ -36,7 +36,24 @@ export function transformJoinResults(rows, operations) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
if (join.localField.includes('.')) {
|
|
40
|
+
const [tableAlias] = join.localField.split('.');
|
|
41
|
+
const relatedJoin = joinOperations.find(j => j.as === tableAlias);
|
|
42
|
+
if (relatedJoin && transformed[relatedJoin.as] !== undefined && transformed[relatedJoin.as] !== null) {
|
|
43
|
+
if (hasAnyData) {
|
|
44
|
+
transformed[relatedJoin.as][join.as] = joinedData;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
transformed[relatedJoin.as][join.as] = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
transformed[join.as] = hasAnyData ? joinedData : null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
transformed[join.as] = hasAnyData ? joinedData : null;
|
|
56
|
+
}
|
|
40
57
|
}
|
|
41
58
|
for (const joinMany of joinManyOperations) {
|
|
42
59
|
const jsonValue = row[joinMany.as];
|
|
@@ -11,4 +11,5 @@ export interface IGenericQueryService<T extends IEntity> {
|
|
|
11
11
|
getAll(userContext: IUserContext): Promise<T[]>;
|
|
12
12
|
get(userContext: IUserContext, queryOptions: IQueryOptions): Promise<IPagedResult<T>>;
|
|
13
13
|
getById(userContext: IUserContext, id: AppIdType): Promise<T>;
|
|
14
|
+
getCount(userContext: IUserContext): Promise<number>;
|
|
14
15
|
}
|
|
@@ -18,4 +18,5 @@ export declare class GenericQueryService<T extends IEntity> implements IGenericQ
|
|
|
18
18
|
getAll(userContext: IUserContext): Promise<T[]>;
|
|
19
19
|
get(userContext: IUserContext, queryOptions?: IQueryOptions): Promise<IPagedResult<T>>;
|
|
20
20
|
getById(userContext: IUserContext, id: AppIdType): Promise<T>;
|
|
21
|
+
getCount(userContext: IUserContext): Promise<number>;
|
|
21
22
|
}
|
|
@@ -44,4 +44,8 @@ export class GenericQueryService {
|
|
|
44
44
|
}
|
|
45
45
|
return this.postProcessEntity(userContext, entity);
|
|
46
46
|
}
|
|
47
|
+
async getCount(userContext) {
|
|
48
|
+
const { operations } = this.prepareQuery(userContext, {}, []);
|
|
49
|
+
return await this.database.getCount(this.rootTableName);
|
|
50
|
+
}
|
|
47
51
|
}
|