@owox/backend 0.25.0 → 0.26.0-next-20260515073306

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.
@@ -210,6 +210,7 @@ let GoogleSheetsReportWriter = GoogleSheetsReportWriter_1 = class GoogleSheetsRe
210
210
  }
211
211
  }
212
212
  async initializeService(report) {
213
+ this.report = report;
213
214
  return this.executeWithErrorHandling(async () => {
214
215
  if (!(0, data_destination_config_guards_1.isGoogleSheetsConfig)(report.destinationConfig)) {
215
216
  throw new Error('Invalid Google Sheets destination configuration provided');
@@ -241,7 +242,6 @@ let GoogleSheetsReportWriter = GoogleSheetsReportWriter_1 = class GoogleSheetsRe
241
242
  this.availableColumnsCount = sheet.properties?.gridProperties?.columnCount ?? 0;
242
243
  this.spreadsheetTimeZone = spreadsheet.properties?.timeZone ?? 'UTC';
243
244
  this.dataMartTitle = report.dataMart.title;
244
- this.report = report;
245
245
  }, 'Initializing Google Sheets service and locating target sheet');
246
246
  }
247
247
  async prepareSheetColumns(columnsCount) {
@@ -1,5 +1,7 @@
1
1
  import { DataMartRelationship } from '../../../entities/data-mart-relationship.entity';
2
2
  import { BlendedQueryContext, ResolvedRelationshipChain } from '../blended-query-builder.interface';
3
3
  export declare function makeRelationship(overrides?: Partial<DataMartRelationship>): DataMartRelationship;
4
- export declare function makeChain(partial: Omit<ResolvedRelationshipChain, 'targetDataMartTitle' | 'targetDataMartUrl'>): ResolvedRelationshipChain;
4
+ export declare function makeChain(partial: Omit<ResolvedRelationshipChain, 'targetDataMartTitle' | 'targetDataMartUrl' | 'cteName'> & {
5
+ cteName?: string;
6
+ }): ResolvedRelationshipChain;
5
7
  export declare function createBuildContext(mainTableReference: string): (chains: ResolvedRelationshipChain[], columns: string[]) => BlendedQueryContext;
@@ -17,8 +17,13 @@ function makeRelationship(overrides = {}) {
17
17
  };
18
18
  }
19
19
  function makeChain(partial) {
20
+ const cteName = partial.cteName ??
21
+ (partial.parentAlias === 'main'
22
+ ? partial.relationship.targetAlias
23
+ : `${partial.parentAlias}_${partial.relationship.targetAlias}`);
20
24
  return {
21
25
  ...partial,
26
+ cteName,
22
27
  targetDataMartTitle: 'Test Subsidiary',
23
28
  targetDataMartUrl: '/ui/proj/data-marts/sub-1/data-setup',
24
29
  };
@@ -53,11 +53,11 @@ class AbstractBlendedQueryBuilder {
53
53
  buildTree(chains) {
54
54
  const nodeMap = new Map();
55
55
  for (const chain of chains) {
56
- nodeMap.set(chain.relationship.targetAlias, { chain, children: [] });
56
+ nodeMap.set(chain.cteName, { chain, children: [] });
57
57
  }
58
58
  const roots = [];
59
59
  for (const chain of chains) {
60
- const node = nodeMap.get(chain.relationship.targetAlias);
60
+ const node = nodeMap.get(chain.cteName);
61
61
  if (chain.parentAlias === 'main') {
62
62
  roots.push(node);
63
63
  }
@@ -75,10 +75,10 @@ class AbstractBlendedQueryBuilder {
75
75
  for (const child of node.children) {
76
76
  const childResult = this.buildSubtreeCtes(child);
77
77
  ctes.push(...childResult.ctes);
78
- childPassthroughs.set(child.chain.relationship.targetAlias, childResult.passthroughFields);
78
+ childPassthroughs.set(child.chain.cteName, childResult.passthroughFields);
79
79
  }
80
80
  const { chain } = node;
81
- const alias = chain.relationship.targetAlias;
81
+ const alias = chain.cteName;
82
82
  const subsidiaryColumns = this.collectSubsidiaryReferences(chain, node.children);
83
83
  ctes.push(this.buildRawCte(`${alias}_raw`, chain.targetTableReference, chain.targetDataMartTitle, chain.targetDataMartUrl, subsidiaryColumns));
84
84
  const hasChildren = node.children.length > 0;
@@ -110,7 +110,7 @@ class AbstractBlendedQueryBuilder {
110
110
  }
111
111
  buildJoinedCte(node, childPassthroughs) {
112
112
  const { chain } = node;
113
- const alias = chain.relationship.targetAlias;
113
+ const alias = chain.cteName;
114
114
  const rawAlias = `${alias}_raw`;
115
115
  const joinedAlias = `${alias}_joined`;
116
116
  const quotedRawAlias = this.quoteIdentifier(rawAlias);
@@ -129,7 +129,7 @@ class AbstractBlendedQueryBuilder {
129
129
  }
130
130
  const joinClauses = [];
131
131
  for (const child of node.children) {
132
- const childAlias = child.chain.relationship.targetAlias;
132
+ const childAlias = child.chain.cteName;
133
133
  const quotedChildAlias = this.quoteIdentifier(childAlias);
134
134
  for (const pt of childPassthroughs.get(childAlias) ?? []) {
135
135
  selectParts.push(`${quotedChildAlias}.${this.quoteIdentifier(pt.outputAlias)}`);
@@ -194,11 +194,11 @@ class AbstractBlendedQueryBuilder {
194
194
  return Array.from(refs).sort();
195
195
  }
196
196
  buildAggregationCte(chain, hasChildren, passthroughFields) {
197
- const { relationship, blendedFields } = chain;
198
- const alias = this.quoteIdentifier(relationship.targetAlias);
197
+ const { relationship, blendedFields, cteName } = chain;
198
+ const alias = this.quoteIdentifier(cteName);
199
199
  const sourceAlias = hasChildren
200
- ? this.quoteIdentifier(`${relationship.targetAlias}_joined`)
201
- : this.quoteIdentifier(`${relationship.targetAlias}_raw`);
200
+ ? this.quoteIdentifier(`${cteName}_joined`)
201
+ : this.quoteIdentifier(`${cteName}_raw`);
202
202
  const parentJoinKeys = relationship.joinConditions.map(jc => this.quoteFieldRef(jc.targetFieldName));
203
203
  const groupByKeys = [...parentJoinKeys];
204
204
  const aggregatedParts = blendedFields.map(field => {
@@ -222,7 +222,7 @@ class AbstractBlendedQueryBuilder {
222
222
  const parts = [];
223
223
  const outputAliasToRoot = new Map();
224
224
  for (const root of roots) {
225
- this.mapOutputAliasesToRoot(root, root.chain.relationship.targetAlias, outputAliasToRoot);
225
+ this.mapOutputAliasesToRoot(root, root.chain.cteName, outputAliasToRoot);
226
226
  }
227
227
  for (const col of columnSet) {
228
228
  const rootAlias = outputAliasToRoot.get(col);
@@ -246,8 +246,8 @@ class AbstractBlendedQueryBuilder {
246
246
  }
247
247
  buildJoinParts(roots) {
248
248
  return roots.map(root => {
249
- const { relationship, parentAlias } = root.chain;
250
- const alias = this.quoteIdentifier(relationship.targetAlias);
249
+ const { relationship, parentAlias, cteName } = root.chain;
250
+ const alias = this.quoteIdentifier(cteName);
251
251
  const parent = this.quoteIdentifier(parentAlias);
252
252
  const onParts = relationship.joinConditions.map(jc => `${parent}.${this.quoteFieldRef(jc.sourceFieldName)} = ${alias}.${this.quoteFieldRef(jc.targetFieldName)}`);
253
253
  const onClause = onParts.join(' AND ');
@@ -14,6 +14,7 @@ export interface ResolvedRelationshipChain {
14
14
  relationship: DataMartRelationship;
15
15
  targetTableReference: string;
16
16
  parentAlias: string;
17
+ cteName: string;
17
18
  blendedFields: BlendedFieldConfig[];
18
19
  targetDataMartTitle: string;
19
20
  targetDataMartUrl: string;
@@ -56,7 +56,7 @@ let BlendedReportDataService = class BlendedReportDataService {
56
56
  const publicOrigin = this.publicOriginService.getPublicOrigin();
57
57
  const mainDataMartUrl = (0, data_mart_url_helper_1.buildDataMartUrl)(publicOrigin, dataMart.projectId, dataMart.id, '/data-setup');
58
58
  const allRelationships = await this.relationshipService.findBySourceDataMartId(dataMart.id);
59
- const chains = await this.buildRelationshipChains(columnConfig, referencedColumns, blendableSchema.blendedFields, blendableSchema.availableSources, allRelationships, dataMart.id, dataMart.projectId, publicOrigin);
59
+ const chains = await this.buildRelationshipChains(columnConfig, referencedColumns, blendableSchema.blendedFields, blendableSchema.availableSources, allRelationships, dataMart.projectId, publicOrigin);
60
60
  const blendedResult = await this.blendedQueryBuilderFacade.buildBlendedQuery(dataMart.storage.type, {
61
61
  mainTableReference,
62
62
  mainDataMartTitle: dataMart.title,
@@ -88,7 +88,7 @@ let BlendedReportDataService = class BlendedReportDataService {
88
88
  }
89
89
  return headers;
90
90
  }
91
- async buildRelationshipChains(columnConfig, referencedColumns, blendedFields, availableSources, directRelationships, rootDataMartId, projectId, publicOrigin) {
91
+ async buildRelationshipChains(columnConfig, referencedColumns, blendedFields, availableSources, directRelationships, projectId, publicOrigin) {
92
92
  const requestedBlendedFields = blendedFields.filter(f => referencedColumns.has(f.name));
93
93
  if (requestedBlendedFields.length === 0) {
94
94
  return [];
@@ -116,25 +116,25 @@ let BlendedReportDataService = class BlendedReportDataService {
116
116
  relationshipsById.set(rel.id, rel);
117
117
  }
118
118
  }
119
- const fieldsByRelId = new Map();
119
+ const fieldsByAliasPath = new Map();
120
120
  for (const field of requestedBlendedFields) {
121
- const bucket = fieldsByRelId.get(field.sourceRelationshipId) ?? [];
121
+ const bucket = fieldsByAliasPath.get(field.aliasPath) ?? [];
122
122
  bucket.push(field);
123
- fieldsByRelId.set(field.sourceRelationshipId, bucket);
123
+ fieldsByAliasPath.set(field.aliasPath, bucket);
124
124
  }
125
125
  const sortedSources = [...neededSources].sort((a, b) => a.depth - b.depth);
126
- const dataMartAliasMap = new Map();
127
- dataMartAliasMap.set(rootDataMartId, 'main');
128
126
  const columnConfigSet = new Set(columnConfig);
129
127
  const chains = [];
130
128
  for (const src of sortedSources) {
131
129
  const rel = relationshipsById.get(src.relationshipId);
132
130
  if (!rel)
133
131
  continue;
134
- const parentAlias = src.depth === 1 ? 'main' : (dataMartAliasMap.get(rel.sourceDataMart.id) ?? 'main');
132
+ const segments = src.aliasPath.split('.');
133
+ const cteName = segments.join('_');
134
+ const parentAlias = segments.length === 1 ? 'main' : segments.slice(0, -1).join('_');
135
135
  const targetTableReference = await this.tableReferenceService.resolveTableName(rel.targetDataMart.id, projectId);
136
136
  const targetDataMartUrl = (0, data_mart_url_helper_1.buildDataMartUrl)(publicOrigin, projectId, rel.targetDataMart.id, '/data-setup');
137
- const chainBlendedFields = (fieldsByRelId.get(rel.id) ?? []).map(f => ({
137
+ const chainBlendedFields = (fieldsByAliasPath.get(src.aliasPath) ?? []).map(f => ({
138
138
  targetFieldName: f.originalFieldName,
139
139
  outputAlias: f.name,
140
140
  isHidden: f.isHidden || !columnConfigSet.has(f.name),
@@ -144,26 +144,28 @@ let BlendedReportDataService = class BlendedReportDataService {
144
144
  relationship: rel,
145
145
  targetTableReference,
146
146
  parentAlias,
147
+ cteName,
147
148
  blendedFields: chainBlendedFields,
148
149
  targetDataMartTitle: rel.targetDataMart.title,
149
150
  targetDataMartUrl,
150
151
  });
151
- dataMartAliasMap.set(rel.targetDataMart.id, rel.targetAlias);
152
152
  }
153
153
  this.assertNoChainCollisions(chains);
154
154
  return chains;
155
155
  }
156
156
  assertNoChainCollisions(chains) {
157
- const aliasOwners = new Map();
157
+ const cteNameOwners = new Map();
158
158
  for (const chain of chains) {
159
- const owners = aliasOwners.get(chain.relationship.targetAlias) ?? [];
159
+ const owners = cteNameOwners.get(chain.cteName) ?? [];
160
160
  owners.push(chain.relationship.id);
161
- aliasOwners.set(chain.relationship.targetAlias, owners);
161
+ cteNameOwners.set(chain.cteName, owners);
162
162
  }
163
- for (const [alias, owners] of aliasOwners) {
163
+ for (const [cteName, owners] of cteNameOwners) {
164
164
  if (owners.length > 1) {
165
- throw new business_violation_exception_1.BusinessViolationException(`Duplicate CTE name: targetAlias "${alias}" is used by multiple relationships in the join chain. ` +
166
- `Rename the targetAlias on one of these relationships so each chain produces a unique CTE.`, { targetAlias: alias, relationshipIds: owners });
165
+ throw new business_violation_exception_1.BusinessViolationException(`Duplicate CTE name: cteName "${cteName}" is produced by multiple relationship chains. ` +
166
+ `This typically means two relationship targetAlias values flatten to the same identifier ` +
167
+ `(e.g. "a_b" at one level vs "a"+"b" at two levels). ` +
168
+ `Rename the targetAlias on one of these relationships so each chain produces a unique CTE.`, { cteName, relationshipIds: owners });
167
169
  }
168
170
  }
169
171
  const outputAliasOwners = new Map();
@@ -1,15 +1,17 @@
1
1
  import { Repository } from 'typeorm';
2
- import { Report } from '../entities/report.entity';
3
- import { DataMart } from '../entities/data-mart.entity';
4
- import { DataMartService } from '../services/data-mart.service';
5
- import { ReportSqlComposerService } from '../services/report-sql-composer.service';
6
2
  import { CopyReportAsDataMartCommand } from '../dto/domain/copy-report-as-data-mart.command';
3
+ import { DataMartDto } from '../dto/domain/data-mart.dto';
4
+ import { Report } from '../entities/report.entity';
7
5
  import { AccessDecisionService } from '../services/access-decision';
6
+ import { ReportSqlComposerService } from '../services/report-sql-composer.service';
7
+ import { CreateDataMartService } from './create-data-mart.service';
8
+ import { UpdateDataMartDefinitionService } from './update-data-mart-definition.service';
8
9
  export declare class CopyReportAsDataMartService {
9
10
  private readonly reportRepository;
10
11
  private readonly reportSqlComposerService;
11
- private readonly dataMartService;
12
+ private readonly createDataMartService;
13
+ private readonly updateDataMartDefinitionService;
12
14
  private readonly accessDecisionService;
13
- constructor(reportRepository: Repository<Report>, reportSqlComposerService: ReportSqlComposerService, dataMartService: DataMartService, accessDecisionService: AccessDecisionService);
14
- run(command: CopyReportAsDataMartCommand): Promise<DataMart>;
15
+ constructor(reportRepository: Repository<Report>, reportSqlComposerService: ReportSqlComposerService, createDataMartService: CreateDataMartService, updateDataMartDefinitionService: UpdateDataMartDefinitionService, accessDecisionService: AccessDecisionService);
16
+ run(command: CopyReportAsDataMartCommand): Promise<DataMartDto>;
15
17
  }
@@ -18,22 +18,26 @@ const typeorm_1 = require("@nestjs/typeorm");
18
18
  const typeorm_2 = require("typeorm");
19
19
  const typeorm_transactional_1 = require("typeorm-transactional");
20
20
  const business_violation_exception_1 = require("../../common/exceptions/business-violation.exception");
21
+ const copy_report_as_data_mart_command_1 = require("../dto/domain/copy-report-as-data-mart.command");
22
+ const create_data_mart_command_1 = require("../dto/domain/create-data-mart.command");
23
+ const update_data_mart_definition_command_1 = require("../dto/domain/update-data-mart-definition.command");
21
24
  const report_entity_1 = require("../entities/report.entity");
22
25
  const data_mart_definition_type_enum_1 = require("../enums/data-mart-definition-type.enum");
23
- const data_mart_status_enum_1 = require("../enums/data-mart-status.enum");
24
- const data_mart_service_1 = require("../services/data-mart.service");
25
- const report_sql_composer_service_1 = require("../services/report-sql-composer.service");
26
- const copy_report_as_data_mart_command_1 = require("../dto/domain/copy-report-as-data-mart.command");
27
26
  const access_decision_1 = require("../services/access-decision");
27
+ const report_sql_composer_service_1 = require("../services/report-sql-composer.service");
28
+ const create_data_mart_service_1 = require("./create-data-mart.service");
29
+ const update_data_mart_definition_service_1 = require("./update-data-mart-definition.service");
28
30
  let CopyReportAsDataMartService = class CopyReportAsDataMartService {
29
31
  reportRepository;
30
32
  reportSqlComposerService;
31
- dataMartService;
33
+ createDataMartService;
34
+ updateDataMartDefinitionService;
32
35
  accessDecisionService;
33
- constructor(reportRepository, reportSqlComposerService, dataMartService, accessDecisionService) {
36
+ constructor(reportRepository, reportSqlComposerService, createDataMartService, updateDataMartDefinitionService, accessDecisionService) {
34
37
  this.reportRepository = reportRepository;
35
38
  this.reportSqlComposerService = reportSqlComposerService;
36
- this.dataMartService = dataMartService;
39
+ this.createDataMartService = createDataMartService;
40
+ this.updateDataMartDefinitionService = updateDataMartDefinitionService;
37
41
  this.accessDecisionService = accessDecisionService;
38
42
  }
39
43
  async run(command) {
@@ -64,19 +68,8 @@ let CopyReportAsDataMartService = class CopyReportAsDataMartService {
64
68
  if (!sql.trim()) {
65
69
  throw new business_violation_exception_1.BusinessViolationException('Unable to copy this report: generated SQL is empty. Ensure the report has a valid column configuration.', { reportId: command.reportId });
66
70
  }
67
- const { dataMart: sourceDataMart } = report;
68
- const definition = { sqlQuery: sql };
69
- const newDataMart = this.dataMartService.create({
70
- title: `Copy of ${report.title}`,
71
- projectId: command.projectId,
72
- createdById: command.userId,
73
- technicalOwnerIds: [command.userId],
74
- storage: sourceDataMart.storage,
75
- definitionType: data_mart_definition_type_enum_1.DataMartDefinitionType.SQL,
76
- definition,
77
- status: data_mart_status_enum_1.DataMartStatus.DRAFT,
78
- });
79
- return this.dataMartService.save(newDataMart);
71
+ const createdDataMart = await this.createDataMartService.run(new create_data_mart_command_1.CreateDataMartCommand(command.projectId, command.userId, `Copy of ${report.title}`, report.dataMart.storage.id, command.roles));
72
+ return this.updateDataMartDefinitionService.run(new update_data_mart_definition_command_1.UpdateDataMartDefinitionCommand(createdDataMart.id, command.projectId, data_mart_definition_type_enum_1.DataMartDefinitionType.SQL, { sqlQuery: sql }, undefined, undefined, command.userId, command.roles));
80
73
  }
81
74
  };
82
75
  exports.CopyReportAsDataMartService = CopyReportAsDataMartService;
@@ -91,7 +84,8 @@ exports.CopyReportAsDataMartService = CopyReportAsDataMartService = __decorate([
91
84
  __param(0, (0, typeorm_1.InjectRepository)(report_entity_1.Report)),
92
85
  __metadata("design:paramtypes", [typeorm_2.Repository,
93
86
  report_sql_composer_service_1.ReportSqlComposerService,
94
- data_mart_service_1.DataMartService,
87
+ create_data_mart_service_1.CreateDataMartService,
88
+ update_data_mart_definition_service_1.UpdateDataMartDefinitionService,
95
89
  access_decision_1.AccessDecisionService])
96
90
  ], CopyReportAsDataMartService);
97
91
  //# sourceMappingURL=copy-report-as-data-mart.service.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owox/backend",
3
- "version": "0.25.0",
3
+ "version": "0.26.0-next-20260515073306",
4
4
  "description": "OWOX Data Marts Backend - Full-stack data orchestration platform",
5
5
  "author": "OWOX",
6
6
  "license": "ELv2",
@@ -78,9 +78,9 @@
78
78
  "@nestjs/schedule": "^6.0.0",
79
79
  "@nestjs/swagger": "^11.2.0",
80
80
  "@nestjs/typeorm": "^11.0.0",
81
- "@owox/connectors": "0.25.0",
82
- "@owox/idp-protocol": "0.25.0",
83
- "@owox/internal-helpers": "0.25.0",
81
+ "@owox/connectors": "0.26.0-next-20260515073306",
82
+ "@owox/idp-protocol": "0.26.0-next-20260515073306",
83
+ "@owox/internal-helpers": "0.26.0-next-20260515073306",
84
84
  "better-sqlite3": "^12.2.0",
85
85
  "class-transformer": "^0.5.1",
86
86
  "class-validator": "^0.14.2",
@@ -139,7 +139,7 @@
139
139
  "ts-node": "^10.9.2",
140
140
  "tsconfig-paths": "^4.2.0",
141
141
  "typeorm-ts-node-commonjs": "^0.3.20",
142
- "@owox/test-utils": "*"
142
+ "@owox/test-utils": "5.0.0-next-20260515073306"
143
143
  },
144
144
  "jest": {
145
145
  "moduleFileExtensions": [