@loomcore/api 0.1.102 → 0.1.104

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.
@@ -8,22 +8,28 @@ function convertFieldToSnakeCase(field) {
8
8
  }
9
9
  return toSnakeCase(field);
10
10
  }
11
- function resolveLocalRef(localField, mainTableName) {
11
+ function resolveLocalRef(localField, mainTableName, operations, currentIndex) {
12
12
  if (!localField.includes('.')) {
13
13
  const snake = convertFieldToSnakeCase(localField);
14
14
  return `"${mainTableName}"."${snake}"`;
15
15
  }
16
16
  const [alias, field] = localField.split('.');
17
17
  const snake = convertFieldToSnakeCase(field);
18
+ const priorOps = operations.slice(0, currentIndex);
19
+ const leftJoinMany = priorOps.find((op) => op instanceof LeftJoinMany && op.as === alias);
20
+ if (leftJoinMany) {
21
+ const castType = field === '_id' || snake === '_id' ? '::int' : '::text';
22
+ return `(SELECT jsonb_array_elements("${alias}")->>'${snake}'${castType})`;
23
+ }
18
24
  return `${alias}."${snake}"`;
19
25
  }
20
26
  async function getTableColumns(client, tableName) {
21
- const result = await client.query(`
22
- SELECT column_name
23
- FROM information_schema.columns
24
- WHERE table_schema = current_schema()
25
- AND table_name = $1
26
- ORDER BY ordinal_position
27
+ const result = await client.query(`
28
+ SELECT column_name
29
+ FROM information_schema.columns
30
+ WHERE table_schema = current_schema()
31
+ AND table_name = $1
32
+ ORDER BY ordinal_position
27
33
  `, [tableName]);
28
34
  return result.rows.map(row => row.column_name);
29
35
  }
@@ -51,17 +57,28 @@ export async function buildSelectClause(client, mainTableName, mainTableAlias, o
51
57
  joinSelects.push(`${join.as}."${col}" AS "${join.as}__${col}"`);
52
58
  }
53
59
  }
54
- for (const joinMany of leftJoinManyOperations) {
60
+ for (let i = 0; i < leftJoinManyOperations.length; i++) {
61
+ const joinMany = leftJoinManyOperations[i];
55
62
  const enrichment = findEnrichmentTarget(joinMany, operations);
56
63
  if (enrichment) {
57
64
  continue;
58
65
  }
59
66
  const manyColumns = await getTableColumns(client, joinMany.from);
60
67
  const foreignSnake = convertFieldToSnakeCase(joinMany.foreignField);
61
- const localRef = resolveLocalRef(joinMany.localField, mainTableName);
68
+ const currentOpIndex = operations.indexOf(joinMany);
69
+ const localRef = resolveLocalRef(joinMany.localField, mainTableName, operations, currentOpIndex);
62
70
  const subAlias = `_sub_${joinMany.as}`;
63
71
  const objParts = manyColumns.map(c => `'${c.replace(/'/g, "''")}', ${subAlias}."${c}"`).join(', ');
64
- const subquery = `(SELECT COALESCE(jsonb_agg(jsonb_build_object(${objParts})), '[]'::jsonb) FROM "${joinMany.from}" AS ${subAlias} WHERE ${subAlias}."${foreignSnake}" = ${localRef})`;
72
+ const isArrayRef = joinMany.localField.includes('.') &&
73
+ operations.slice(0, currentOpIndex).some(op => op instanceof LeftJoinMany && op.as === joinMany.localField.split('.')[0]);
74
+ let whereClause;
75
+ if (isArrayRef) {
76
+ whereClause = `${subAlias}."${foreignSnake}" IN ${localRef}`;
77
+ }
78
+ else {
79
+ whereClause = `${subAlias}."${foreignSnake}" = ${localRef}`;
80
+ }
81
+ const subquery = `(SELECT COALESCE(jsonb_agg(jsonb_build_object(${objParts})), '[]'::jsonb) FROM "${joinMany.from}" AS ${subAlias} WHERE ${whereClause})`;
65
82
  joinSelects.push(`${subquery} AS "${joinMany.as}"`);
66
83
  }
67
84
  const allSelects = [...mainSelects, ...joinSelects];
@@ -1,8 +1,8 @@
1
1
  export async function doesTableExist(client, tableName) {
2
- const result = await client.query(`
3
- SELECT EXISTS (
4
- SELECT 1 FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = $1
5
- )
2
+ const result = await client.query(`
3
+ SELECT EXISTS (
4
+ SELECT 1 FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = $1
5
+ )
6
6
  `, [tableName]);
7
7
  return result.rows[0].exists;
8
8
  }
@@ -15,28 +15,45 @@ function parseJsonValue(value) {
15
15
  }
16
16
  return value;
17
17
  }
18
- function getJoinAliases(operations) {
18
+ function getJoinAliasesAndParents(operations) {
19
19
  const aliases = new Set();
20
+ const parentByAlias = new Map();
20
21
  for (const op of operations) {
21
22
  if (op instanceof LeftJoin || op instanceof InnerJoin || op instanceof LeftJoinMany) {
22
23
  aliases.add(op.as);
24
+ const parent = op.localField.includes('.') ? op.localField.split('.')[0] : null;
25
+ parentByAlias.set(op.as, parent);
23
26
  }
24
27
  }
25
- return aliases;
28
+ return { aliases, parentByAlias };
26
29
  }
27
30
  const PREFIX_SEP = '__';
31
+ function setJoinValue(joinData, alias, value, parentByAlias) {
32
+ const parent = parentByAlias.get(alias) ?? null;
33
+ if (parent) {
34
+ let parentObj = joinData[parent];
35
+ if (parentObj == null || typeof parentObj !== 'object' || Array.isArray(parentObj)) {
36
+ parentObj = {};
37
+ joinData[parent] = parentObj;
38
+ }
39
+ parentObj[alias] = value;
40
+ }
41
+ else {
42
+ joinData[alias] = value;
43
+ }
44
+ }
28
45
  export function transformJoinResults(rows, operations) {
29
- const joinAliases = getJoinAliases(operations);
46
+ const { aliases: joinAliases, parentByAlias } = getJoinAliasesAndParents(operations);
30
47
  if (joinAliases.size === 0) {
31
48
  return rows;
32
49
  }
33
50
  return rows.map((row) => {
34
51
  const transformed = {};
35
- const joinData = {};
52
+ const flatJoinValues = {};
36
53
  const prefixedByAlias = {};
37
54
  for (const key of Object.keys(row)) {
38
55
  if (joinAliases.has(key)) {
39
- joinData[key] = parseJsonValue(row[key]);
56
+ flatJoinValues[key] = parseJsonValue(row[key]);
40
57
  }
41
58
  else if (key.includes(PREFIX_SEP)) {
42
59
  const i = key.indexOf(PREFIX_SEP);
@@ -58,10 +75,20 @@ export function transformJoinResults(rows, operations) {
58
75
  for (const alias of Object.keys(prefixedByAlias)) {
59
76
  const obj = prefixedByAlias[alias];
60
77
  const hasAny = Object.values(obj).some(v => v !== null && v !== undefined);
61
- if (!(alias in joinData)) {
62
- joinData[alias] = hasAny ? obj : null;
78
+ if (!(alias in flatJoinValues)) {
79
+ flatJoinValues[alias] = hasAny ? obj : null;
63
80
  }
64
81
  }
82
+ const joinData = {};
83
+ for (const op of operations) {
84
+ if (!(op instanceof LeftJoin || op instanceof InnerJoin || op instanceof LeftJoinMany))
85
+ continue;
86
+ const alias = op.as;
87
+ const value = flatJoinValues[alias];
88
+ if (value === undefined)
89
+ continue;
90
+ setJoinValue(joinData, alias, value, parentByAlias);
91
+ }
65
92
  if (Object.keys(joinData).length > 0) {
66
93
  transformed._joinData = joinData;
67
94
  }
package/package.json CHANGED
@@ -1,92 +1,92 @@
1
- {
2
- "name": "@loomcore/api",
3
- "version": "0.1.102",
4
- "private": false,
5
- "description": "Loom Core Api - An opinionated Node.js api using Typescript, Express, and MongoDb or PostgreSQL",
6
- "scripts": {
7
- "clean": "rm -rf dist",
8
- "tsc": "tsc --project tsconfig.prod.json",
9
- "build": "npm-run-all -s clean tsc",
10
- "add": "git add .",
11
- "commit": "git commit -m \"Updates\"",
12
- "patch": "npm version patch",
13
- "push": "git push",
14
- "publishMe": "npm publish --access public",
15
- "pub": "npm-run-all -s add commit patch build push publishMe",
16
- "update-lib-versions": "npx --yes npm-check-updates -u -f @loomcore/common",
17
- "install-updated-libs": "npm i @loomcore/common",
18
- "update-libs": "npm-run-all -s update-lib-versions install-updated-libs",
19
- "typecheck": "tsc",
20
- "test": "npm-run-all -s test:postgres test:mongodb",
21
- "test:postgres": "cross-env NODE_ENV=test TEST_DATABASE=postgres vitest run",
22
- "test:postgres:real": "cross-env NODE_ENV=test TEST_DATABASE=postgres USE_REAL_POSTGRES=true vitest run",
23
- "test:mongodb": "cross-env NODE_ENV=test TEST_DATABASE=mongodb vitest run",
24
- "test:ci": "npm-run-all -s test:ci:postgres test:ci:mongodb",
25
- "test:ci:postgres": "cross-env NODE_ENV=test TEST_DATABASE=postgres vitest run --reporter=json --outputFile=test-results-postgres.json",
26
- "test:ci:mongodb": "cross-env NODE_ENV=test TEST_DATABASE=mongodb vitest run --reporter=json --outputFile=test-results-mongodb.json",
27
- "test:watch": "cross-env NODE_ENV=test vitest",
28
- "coverage": "npm-run-all -s coverage:postgres coverage:mongodb",
29
- "coverage:postgres": "cross-env NODE_ENV=test TEST_DATABASE=postgres vitest run --coverage",
30
- "coverage:mongodb": "cross-env NODE_ENV=test TEST_DATABASE=mongodb vitest run --coverage",
31
- "test:db:start": "docker-compose -f docker-compose.test.yml up -d",
32
- "test:db:stop": "docker-compose -f docker-compose.test.yml down",
33
- "test:db:logs": "docker-compose -f docker-compose.test.yml logs -f"
34
- },
35
- "author": "Tim Hardy",
36
- "license": "Apache 2.0",
37
- "main": "dist/index.js",
38
- "type": "module",
39
- "types": "dist/index.d.ts",
40
- "files": [
41
- "dist/**/*"
42
- ],
43
- "exports": {
44
- "./__tests__": "./dist/__tests__/index.js",
45
- "./config": "./dist/config/index.js",
46
- "./controllers": "./dist/controllers/index.js",
47
- "./databases": "./dist/databases/index.js",
48
- "./errors": "./dist/errors/index.js",
49
- "./middleware": "./dist/middleware/index.js",
50
- "./models": "./dist/models/index.js",
51
- "./services": "./dist/services/index.js",
52
- "./utils": "./dist/utils/index.js"
53
- },
54
- "dependencies": {
55
- "jsonwebtoken": "^9.0.2",
56
- "node-mailjet": "^6.0.8",
57
- "qs": "^6.15.0"
58
- },
59
- "peerDependencies": {
60
- "@loomcore/common": "^0.0.51",
61
- "@sinclair/typebox": "0.34.33",
62
- "cookie-parser": "^1.4.6",
63
- "cors": "^2.8.5",
64
- "express": "^5.1.0",
65
- "lodash": "^4.17.21",
66
- "moment": "^2.30.1",
67
- "mongodb": "^6.16.0",
68
- "pg": "^8.15.6",
69
- "rxjs": "^7.8.0",
70
- "umzug": "^3.8.2"
71
- },
72
- "devDependencies": {
73
- "@types/cookie-parser": "^1.4.7",
74
- "@types/cors": "^2.8.18",
75
- "@types/express": "^5.0.1",
76
- "@types/jsonwebtoken": "^9.0.9",
77
- "@types/lodash": "^4.17.13",
78
- "@types/pg": "^8.15.6",
79
- "@types/qs": "^6.14.0",
80
- "@types/supertest": "^6.0.3",
81
- "@vitest/coverage-v8": "^3.0.9",
82
- "cross-env": "^7.0.3",
83
- "mongodb-memory-server": "^9.3.0",
84
- "npm-run-all": "^4.1.5",
85
- "pg-mem": "^3.0.12",
86
- "rxjs": "^7.8.0",
87
- "supertest": "^7.1.0",
88
- "typescript": "^5.8.3",
89
- "vite": "^6.2.5",
90
- "vitest": "^3.0.9"
91
- }
92
- }
1
+ {
2
+ "name": "@loomcore/api",
3
+ "version": "0.1.104",
4
+ "private": false,
5
+ "description": "Loom Core Api - An opinionated Node.js api using Typescript, Express, and MongoDb or PostgreSQL",
6
+ "scripts": {
7
+ "clean": "rm -rf dist",
8
+ "tsc": "tsc --project tsconfig.prod.json",
9
+ "build": "npm-run-all -s clean tsc",
10
+ "add": "git add .",
11
+ "commit": "git commit -m \"Updates\"",
12
+ "patch": "npm version patch",
13
+ "push": "git push",
14
+ "publishMe": "npm publish --access public",
15
+ "pub": "npm-run-all -s add commit patch build push publishMe",
16
+ "update-lib-versions": "npx --yes npm-check-updates -u -f @loomcore/common",
17
+ "install-updated-libs": "npm i @loomcore/common",
18
+ "update-libs": "npm-run-all -s update-lib-versions install-updated-libs",
19
+ "typecheck": "tsc",
20
+ "test": "npm-run-all -s test:postgres test:mongodb",
21
+ "test:postgres": "cross-env NODE_ENV=test TEST_DATABASE=postgres vitest run",
22
+ "test:postgres:real": "cross-env NODE_ENV=test TEST_DATABASE=postgres USE_REAL_POSTGRES=true vitest run",
23
+ "test:mongodb": "cross-env NODE_ENV=test TEST_DATABASE=mongodb vitest run",
24
+ "test:ci": "npm-run-all -s test:ci:postgres test:ci:mongodb",
25
+ "test:ci:postgres": "cross-env NODE_ENV=test TEST_DATABASE=postgres vitest run --reporter=json --outputFile=test-results-postgres.json",
26
+ "test:ci:mongodb": "cross-env NODE_ENV=test TEST_DATABASE=mongodb vitest run --reporter=json --outputFile=test-results-mongodb.json",
27
+ "test:watch": "cross-env NODE_ENV=test vitest",
28
+ "coverage": "npm-run-all -s coverage:postgres coverage:mongodb",
29
+ "coverage:postgres": "cross-env NODE_ENV=test TEST_DATABASE=postgres vitest run --coverage",
30
+ "coverage:mongodb": "cross-env NODE_ENV=test TEST_DATABASE=mongodb vitest run --coverage",
31
+ "test:db:start": "docker-compose -f docker-compose.test.yml up -d",
32
+ "test:db:stop": "docker-compose -f docker-compose.test.yml down",
33
+ "test:db:logs": "docker-compose -f docker-compose.test.yml logs -f"
34
+ },
35
+ "author": "Tim Hardy",
36
+ "license": "Apache 2.0",
37
+ "main": "dist/index.js",
38
+ "type": "module",
39
+ "types": "dist/index.d.ts",
40
+ "files": [
41
+ "dist/**/*"
42
+ ],
43
+ "exports": {
44
+ "./__tests__": "./dist/__tests__/index.js",
45
+ "./config": "./dist/config/index.js",
46
+ "./controllers": "./dist/controllers/index.js",
47
+ "./databases": "./dist/databases/index.js",
48
+ "./errors": "./dist/errors/index.js",
49
+ "./middleware": "./dist/middleware/index.js",
50
+ "./models": "./dist/models/index.js",
51
+ "./services": "./dist/services/index.js",
52
+ "./utils": "./dist/utils/index.js"
53
+ },
54
+ "dependencies": {
55
+ "jsonwebtoken": "^9.0.2",
56
+ "node-mailjet": "^6.0.8",
57
+ "qs": "^6.15.0"
58
+ },
59
+ "peerDependencies": {
60
+ "@loomcore/common": "^0.0.51",
61
+ "@sinclair/typebox": "0.34.33",
62
+ "cookie-parser": "^1.4.6",
63
+ "cors": "^2.8.5",
64
+ "express": "^5.1.0",
65
+ "lodash": "^4.17.21",
66
+ "moment": "^2.30.1",
67
+ "mongodb": "^6.16.0",
68
+ "pg": "^8.15.6",
69
+ "rxjs": "^7.8.0",
70
+ "umzug": "^3.8.2"
71
+ },
72
+ "devDependencies": {
73
+ "@types/cookie-parser": "^1.4.7",
74
+ "@types/cors": "^2.8.18",
75
+ "@types/express": "^5.0.1",
76
+ "@types/jsonwebtoken": "^9.0.9",
77
+ "@types/lodash": "^4.17.13",
78
+ "@types/pg": "^8.15.6",
79
+ "@types/qs": "^6.14.0",
80
+ "@types/supertest": "^6.0.3",
81
+ "@vitest/coverage-v8": "^3.0.9",
82
+ "cross-env": "^7.0.3",
83
+ "mongodb-memory-server": "^9.3.0",
84
+ "npm-run-all": "^4.1.5",
85
+ "pg-mem": "^3.0.12",
86
+ "rxjs": "^7.8.0",
87
+ "supertest": "^7.1.0",
88
+ "typescript": "^5.8.3",
89
+ "vite": "^6.2.5",
90
+ "vitest": "^3.0.9"
91
+ }
92
+ }