bd-orm 0.0.1

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 (59) hide show
  1. package/.github/workflows/build-and-test.yml +26 -0
  2. package/.github/workflows/publish.yml +36 -0
  3. package/.prettierrc.json +14 -0
  4. package/CHANGELOG.md +3 -0
  5. package/README.md +5 -0
  6. package/check NBR en TODO.txt +32 -0
  7. package/dist/_/BdORMBase.d.ts +111 -0
  8. package/dist/_/BdORMBase.js +262 -0
  9. package/dist/_/BdORMCrud.d.ts +51 -0
  10. package/dist/_/BdORMCrud.js +223 -0
  11. package/dist/_/BdORMError.d.ts +4 -0
  12. package/dist/_/BdORMError.js +14 -0
  13. package/dist/_/BdOrmConnection.d.ts +32 -0
  14. package/dist/_/BdOrmConnection.js +5 -0
  15. package/dist/_/cdsBaseOrm.d.ts +12 -0
  16. package/dist/_/cdsBaseOrm.js +21 -0
  17. package/dist/dbConnections/BdOrmDbConnectionCapHana/index.d.ts +26 -0
  18. package/dist/dbConnections/BdOrmDbConnectionCapHana/index.js +78 -0
  19. package/dist/dbConnections/BdOrmDbConnectionCapHana/utils.d.ts +14 -0
  20. package/dist/dbConnections/BdOrmDbConnectionCapHana/utils.js +49 -0
  21. package/dist/dbConnections/BdOrmDbConnectionCapSqlite/index.d.ts +22 -0
  22. package/dist/dbConnections/BdOrmDbConnectionCapSqlite/index.js +85 -0
  23. package/dist/dbConnections/BdOrmDbConnectionCapSqlite/utils.d.ts +3 -0
  24. package/dist/dbConnections/BdOrmDbConnectionCapSqlite/utils.js +74 -0
  25. package/dist/dbConnections/BdOrmDbConnectionSQLBase.d.ts +32 -0
  26. package/dist/dbConnections/BdOrmDbConnectionSQLBase.js +99 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.js +18 -0
  29. package/dist/test-assets/Post.d.ts +12 -0
  30. package/dist/test-assets/Post.js +13 -0
  31. package/dist/test-assets/User.d.ts +16 -0
  32. package/dist/test-assets/User.js +13 -0
  33. package/dist/test-assets/jestUtils.d.ts +5 -0
  34. package/dist/test-assets/jestUtils.js +30 -0
  35. package/jest.config.js +7 -0
  36. package/package.json +53 -0
  37. package/src/_/BdORMBase.ts +288 -0
  38. package/src/_/BdORMCrud.test.ts +198 -0
  39. package/src/_/BdORMCrud.ts +240 -0
  40. package/src/_/BdORMError.ts +10 -0
  41. package/src/_/BdORMbase.test.ts +166 -0
  42. package/src/_/BdOrmConnection.ts +39 -0
  43. package/src/_/cdsBaseOrm.ts +18 -0
  44. package/src/dbConnections/BdOrmDbConnectionCapHana/index.ts +95 -0
  45. package/src/dbConnections/BdOrmDbConnectionCapHana/utils.ts +47 -0
  46. package/src/dbConnections/BdOrmDbConnectionCapHana.test.ts +18 -0
  47. package/src/dbConnections/BdOrmDbConnectionCapSqlite/index.ts +89 -0
  48. package/src/dbConnections/BdOrmDbConnectionCapSqlite/utils.ts +75 -0
  49. package/src/dbConnections/BdOrmDbConnectionCapSqlite.test.ts +41 -0
  50. package/src/dbConnections/BdOrmDbConnectionSQLBase.ts +124 -0
  51. package/src/index.ts +8 -0
  52. package/src/test-assets/Post.ts +16 -0
  53. package/src/test-assets/User.ts +18 -0
  54. package/src/test-assets/db/BdOrm.cds +20 -0
  55. package/src/test-assets/db/csv/bdorm-user.csv +3 -0
  56. package/src/test-assets/db/views/user.hdbview +3 -0
  57. package/src/test-assets/jestUtils.ts +24 -0
  58. package/tsconfig.json +16 -0
  59. package/validate-package.js +113 -0
@@ -0,0 +1,240 @@
1
+ import BdORMBase, { definePropertyDescriptors } from './BdORMBase';
2
+ import { BdORMError } from './BdORMError';
3
+ import type BdOrmDbConnection from './BdOrmConnection';
4
+
5
+ type MethodParams<K extends string> = {
6
+ [key in K]?: any;
7
+ };
8
+
9
+ export type BdORMDuplicateNewDataMethod = (v: any) => string | boolean | null | number;
10
+ export interface BdORMDuplicateParams {
11
+ asDraft?: boolean; // if true, the duplicate will be created as a draft and not saved to the db
12
+ newData?: { [key: string]: string | boolean | null | number | BdORMDuplicateNewDataMethod };
13
+ excludeColumns?: string[];
14
+ }
15
+
16
+ const _filerDataByTable = async (self: BdORMCrud): Promise<Record<string, any>> => {
17
+ const data = self.getData(true);
18
+ const columns = await (self.constructor as typeof BdORMCrud).getDbConnection().getTableColumns(self.getTable());
19
+ return Object.keys(data).reduce((acc, key) => {
20
+ if (columns.includes(key)) acc[key] = data[key];
21
+ return acc;
22
+ }, {} as Record<string, any>);
23
+ };
24
+
25
+ export default class BdORMCrud extends BdORMBase {
26
+ private static _DbConnection: InstanceType<typeof BdOrmDbConnection>;
27
+
28
+ // Events
29
+ protected async beforeCreate<K extends string>(beforeCreateParams?: MethodParams<K>) {
30
+ /* Overschrijf deze functie indien nodig */
31
+ }
32
+ protected async beforeUpdate<K extends string>(beforeUpdateParams?: MethodParams<K>) {
33
+ /* Overschrijf deze functie indien nodig */
34
+ }
35
+ protected async beforeSave<K extends string>(beforeSaveParams?: MethodParams<K>) {
36
+ /* Overschrijf deze functie indien nodig */
37
+ }
38
+ protected async afterCreate<K extends string>(afterCreateParams?: MethodParams<K>) {
39
+ /* Overschrijf deze functie indien nodig */
40
+ }
41
+ protected async afterUpdate<K extends string>(afterUpdateParams?: MethodParams<K>) {
42
+ /* Overschrijf deze functie indien nodig */
43
+ }
44
+ protected async afterSave<K extends string>(afterSaveParams?: MethodParams<K>) {
45
+ /* Overschrijf deze functie indien nodig */
46
+ }
47
+ protected async beforeDelete() {}
48
+ protected async afterDelete() {}
49
+
50
+ private _execDBConnectionMethod(
51
+ method: 'create',
52
+ table: string,
53
+ data: Record<string, any>,
54
+ ): Promise<Record<string, any>>;
55
+ private _execDBConnectionMethod(
56
+ method: 'update',
57
+ table: string,
58
+ data: Record<string, any>,
59
+ options?: { primaryKey: string },
60
+ ): Promise<Record<string, any>>;
61
+ private _execDBConnectionMethod(
62
+ method: 'delete',
63
+ table: string,
64
+ primaryKeyValue: string | number,
65
+ options?: { primaryKey: string },
66
+ ): Promise<unknown>;
67
+ private _execDBConnectionMethod(method: keyof BdOrmDbConnection, ...args: any[]): Promise<any> {
68
+ //@ts-ignore
69
+ return (this.constructor as typeof BdORMCrud)._DbConnection[method](...args);
70
+ }
71
+
72
+ private async _create({ preventBeforeCreate = false, preventAfterCreate = false } = {}): Promise<
73
+ Record<string, any>
74
+ > {
75
+ if (!preventBeforeCreate) await this.beforeCreate();
76
+ const data = await _filerDataByTable(this),
77
+ primaryKey = this.getPrimaryKey();
78
+ if (primaryKey in data) delete data[primaryKey];
79
+ const result = { ...(await this._execDBConnectionMethod('create', this.getTable(), data)) };
80
+ const primaryKeyValue = primaryKey in result ? result[primaryKey] : false;
81
+ if (primaryKeyValue) delete result[primaryKey];
82
+ // when creating a new entry, it is not necessary to add all columns
83
+ // and with an insert, you get all columns back from the db
84
+ // so recreate the propertyDescriptors
85
+ definePropertyDescriptors(this, result);
86
+ // important AFTER definePropertyDescriptors
87
+ if (primaryKeyValue) this.setProperty(primaryKey, primaryKeyValue);
88
+ if (!preventAfterCreate) await this.afterCreate();
89
+ this._setChangedProperties([]);
90
+ return {
91
+ [primaryKey]: primaryKeyValue,
92
+ ...result,
93
+ };
94
+ }
95
+
96
+ private async _update({ preventBeforeUpdate = false, preventAfterUpdate = false } = {}): Promise<void> {
97
+ if (this.isNew()) throw new BdORMError('Cannot update an new instance');
98
+ if (!preventBeforeUpdate) await this.beforeUpdate();
99
+ const data = await _filerDataByTable(this);
100
+ await this._execDBConnectionMethod('update', this.getTable(), data, {
101
+ primaryKey: this.getPrimaryKey(),
102
+ });
103
+ if (!preventAfterUpdate) await this.afterUpdate();
104
+ }
105
+
106
+ /**
107
+ * Delete an instance from the database
108
+ */
109
+ async delete(): Promise<void> {
110
+ (this.constructor as typeof BdORMCrud).isMethodAllowed('delete', { throwErrorIfNotAllowed: true });
111
+ if (!this.id) {
112
+ throw new BdORMError('Cannot delete an instance without an id');
113
+ }
114
+ await this.beforeDelete();
115
+ await this._execDBConnectionMethod('delete', this.getTable(), this.getPrimaryKeyValue(), {
116
+ primaryKey: this.getPrimaryKey(),
117
+ });
118
+ await this.afterDelete();
119
+ }
120
+
121
+ /**
122
+ * Duplicates the current instance and if the options asDraft is not defined or false it will be saved it to the database
123
+ */
124
+ async duplicate({ asDraft = false, newData = {}, excludeColumns = [] }: BdORMDuplicateParams = {}): Promise<
125
+ InstanceType<typeof BdORMCrud>
126
+ > {
127
+ const self = this.constructor as typeof BdORMCrud;
128
+ self.isMethodAllowed('duplicate', { throwErrorIfNotAllowed: true });
129
+ if (!asDraft && this.isNew()) throw new BdORMError('Cannot duplicate an new instance');
130
+ const data = { ...this.getData(true) };
131
+ const primaryKey = this.getPrimaryKey();
132
+ if (primaryKey in data) delete data[primaryKey];
133
+ for (const key in data) {
134
+ if (excludeColumns.includes(key) || self.PROPERTIES_NOT_ALLOWED_TO_DUPLICATE.includes(key)) {
135
+ delete data[key];
136
+ continue;
137
+ }
138
+ if (key in newData) data[key] = typeof newData[key] === 'function' ? newData[key](data[key]) : newData[key];
139
+ }
140
+ const newInstance = new self(data);
141
+ if (!asDraft) await newInstance.save({ preventBeforeSave: true, preventAfterSave: true });
142
+ return newInstance;
143
+ }
144
+
145
+ async save({
146
+ beforeSaveParams = {},
147
+ afterSaveParams = {},
148
+ preventAfterSave = false,
149
+ preventBeforeSave = false,
150
+ } = {}): Promise<InstanceType<typeof BdORMCrud>> {
151
+ (this.constructor as typeof BdORMCrud).isMethodAllowed('save', { throwErrorIfNotAllowed: true });
152
+ await this.beforeSave(beforeSaveParams);
153
+ if (this.isNew()) {
154
+ await this._create({ preventBeforeCreate: preventBeforeSave, preventAfterCreate: preventAfterSave });
155
+ } else {
156
+ await this._update({ preventBeforeUpdate: preventBeforeSave, preventAfterUpdate: preventAfterSave });
157
+ }
158
+ if (!preventAfterSave) {
159
+ await this.afterSave(afterSaveParams);
160
+ }
161
+ return this;
162
+ }
163
+
164
+ // STATICS
165
+
166
+ static async count(where?: Record<string, any> | string, values?: any[]): Promise<number> {
167
+ const result = await this._DbConnection.list(this.VIEW, {
168
+ columns: 'COUNT(*) as count',
169
+ filters: where,
170
+ values,
171
+ });
172
+ return result[0].count;
173
+ }
174
+
175
+ static async create<T extends typeof BdORMCrud>(this: T, data: any): Promise<InstanceType<T>> {
176
+ const instance = new this(data) as InstanceType<T>;
177
+ await instance.save();
178
+ return instance;
179
+ }
180
+
181
+ public static async fetch<T extends typeof BdORMCrud>(
182
+ this: T,
183
+ where?: Record<string, any> | string,
184
+ values?: any[],
185
+ ): Promise<InstanceType<T>[]> {
186
+ const results = await this._DbConnection.list(this.VIEW, {
187
+ columns: this.COLUMNS_TO_FETCH as string,
188
+ filters: where,
189
+ values,
190
+ });
191
+ return results.map((result: Record<string, any>) => new this(result) as InstanceType<T>);
192
+ }
193
+
194
+ public static async fetchById<T extends typeof BdORMCrud>(
195
+ this: T,
196
+ id: string | number,
197
+ ): Promise<InstanceType<T> | undefined> {
198
+ return this.fetchByPrimaryKey(id);
199
+ }
200
+
201
+ public static async fetchOne<T extends typeof BdORMCrud>(
202
+ this: T,
203
+ where: Record<string, any> | string,
204
+ values?: any[],
205
+ ): Promise<InstanceType<T> | undefined> {
206
+ let limit;
207
+ if (typeof where === 'string') where += ' LIMIT 1';
208
+ else limit = 1;
209
+ const results = await this._DbConnection.list(this.VIEW, {
210
+ columns: this.COLUMNS_TO_FETCH as string,
211
+ filters: where,
212
+ limit,
213
+ values,
214
+ });
215
+ return results?.length ? (new this(results[0]) as InstanceType<T>) : undefined;
216
+ }
217
+
218
+ public static async fetchByPrimaryKey<T extends typeof BdORMCrud>(
219
+ this: T,
220
+ primaryKeyValue: string | number,
221
+ ): Promise<InstanceType<T> | undefined> {
222
+ const primaryKey = await this._DbConnection.getTablePrimaryKey(this.TABLE);
223
+ const result = await this._DbConnection.read(this.VIEW, primaryKeyValue, { primaryKey });
224
+ if (!result)
225
+ throw new BdORMError(`No entry found for ${this.VIEW} with ${this.PRIMARY_KEY} ${primaryKeyValue}`);
226
+ return new this(result) as InstanceType<T>;
227
+ }
228
+
229
+ public static setDbConnection(dbConnection: InstanceType<typeof BdOrmDbConnection>) {
230
+ if (this._DbConnection) throw new BdORMError('DbConnection already set');
231
+ this._DbConnection = dbConnection;
232
+ }
233
+ public static clearDbConnection() {
234
+ //@ts-ignore
235
+ this._DbConnection = undefined;
236
+ }
237
+ public static getDbConnection() {
238
+ return this._DbConnection;
239
+ }
240
+ }
@@ -0,0 +1,10 @@
1
+ export class BdORMError extends Error {
2
+ constructor(msg: string) {
3
+ super(`[bd-Orm Error]: ${msg}`);
4
+ Error.captureStackTrace(this, BdORMError);
5
+ this.name = 'BdORMError';
6
+ }
7
+ static staticNotImplemented(methodName: string) {
8
+ return new BdORMError(`static ${methodName} not implemented! Set in derived class`);
9
+ }
10
+ }
@@ -0,0 +1,166 @@
1
+ import { expectErrorMessage, expectToBes } from '../test-assets/jestUtils';
2
+ import BaseORM from './BdORMBase';
3
+ import { BdORMError } from './BdORMError';
4
+
5
+ describe('bd-orm baseORM', () => {
6
+ describe('Errors if implementation has not been done', () => {
7
+ const staticGetters = ['TABLE'];
8
+ class SubTestWithOutImplementations extends BaseORM {}
9
+ it('Static getters', () => {
10
+ staticGetters.forEach(method => {
11
+ expectErrorMessage(async () => {
12
+ //@ts-ignore
13
+ SubTestWithOutImplementations[method];
14
+ }, BdORMError.staticNotImplemented(`get ${method}()`).message);
15
+ });
16
+ });
17
+ });
18
+
19
+ describe('public getters', () => {
20
+ interface Sub {
21
+ name: string;
22
+ age: number;
23
+ }
24
+ class Sub extends BaseORM {
25
+ public static PROPERTIES_NOT_ALLOWED_TO_CHANGE = ['name'];
26
+ public static DB_TABLE = 'Sub_TABLE';
27
+ }
28
+ const newSubInstance = new Sub({
29
+ name: 'Barry Dam',
30
+ age: 38,
31
+ });
32
+
33
+ // All tests below are in case of a NEW entry
34
+
35
+ expectToBes({
36
+ 'getTable()': {
37
+ test: () => newSubInstance.getTable(),
38
+ toBe: 'Sub_TABLE',
39
+ },
40
+ VIEW: { test: () => Sub.VIEW, toBe: 'Sub_TABLE' },
41
+ getPrimaryKey: { test: () => newSubInstance.getPrimaryKey(), toBe: 'id' },
42
+ getPrimaryKeyValue: { test: () => newSubInstance.getPrimaryKeyValue(), toBe: 0 },
43
+ getProperty: {
44
+ test: () => newSubInstance.getProperty('name'),
45
+ toBe: 'Barry Dam',
46
+ },
47
+ getData: {
48
+ test: () => JSON.stringify(newSubInstance.getData()),
49
+ toBe: JSON.stringify({ name: 'Barry Dam', age: 38, id: 0 }),
50
+ },
51
+ getModelName: { test: () => Sub.getModelName(), toBe: 'Sub' },
52
+ setProperty: {
53
+ test: () => {
54
+ newSubInstance.setProperty('age', 40);
55
+ const a = newSubInstance.getProperty('age'),
56
+ b = newSubInstance.age;
57
+ return b === a && a === 40;
58
+ },
59
+ toBe: true,
60
+ },
61
+ isNew: {
62
+ test: () => newSubInstance.isNew(),
63
+ toBe: true,
64
+ },
65
+ name: {
66
+ test: () => {
67
+ newSubInstance.name = 'John Doe';
68
+ return newSubInstance.getProperty('name');
69
+ },
70
+ toBe: 'John Doe',
71
+ },
72
+ });
73
+
74
+ // All tests below are in case of a existing entry
75
+ const existingSubInstance = new Sub({
76
+ id: 1,
77
+ name: 'Barry Dam',
78
+ age: 38,
79
+ });
80
+
81
+ it("Should not allow to change the primary key after it's set", () => {
82
+ expectErrorMessage(async () => {
83
+ existingSubInstance.id = 2;
84
+ }, 'Primary key cannot be changed from 1 to 2');
85
+ });
86
+
87
+ it("Should not allow to change the name after it's set", () => {
88
+ expectErrorMessage(async () => {
89
+ existingSubInstance.setProperty('name', 'new name');
90
+ }, 'Property "name" is not allowed to be changed');
91
+ expectErrorMessage(async () => {
92
+ existingSubInstance.name = 'new name';
93
+ }, 'Property "name" is not allowed to be changed');
94
+ });
95
+ });
96
+
97
+ describe('Property changed checks', () => {
98
+ class PropChanged extends BaseORM {
99
+ public static get TABLE(): string {
100
+ return 'PropChanged_TABLE';
101
+ }
102
+
103
+ static get PROPERTIES_NOT_ALLOWED_TO_CHANGE() {
104
+ return ['age'];
105
+ }
106
+ }
107
+ const propChangedNewInstance = new PropChanged({
108
+ name: 'Barry Dam',
109
+ age: 38,
110
+ }),
111
+ propChangedExistingInstance = new PropChanged({
112
+ id: 1,
113
+ name: 'Barry Dam',
114
+ age: 38,
115
+ });
116
+ it('hasChanges checks', () => {
117
+ expect(propChangedNewInstance.hasChanges()).toBe(false);
118
+ propChangedNewInstance.name = 'John Doe';
119
+ // since the name has changed, but the instance is new, it should still return false
120
+ expect(propChangedNewInstance.hasChanges()).toBe(false);
121
+ // existing instance fetched from db should not have changes
122
+ expect(propChangedExistingInstance.hasChanges()).toBe(false);
123
+ // same name as before, so no changes
124
+ propChangedExistingInstance.name = 'Barry Dam';
125
+ expect(propChangedExistingInstance.hasChanges()).toBe(false);
126
+ // name has changed, so changes
127
+ propChangedExistingInstance.name = 'John Doe';
128
+ expect(propChangedExistingInstance.hasChanges()).toBe(true);
129
+ });
130
+ it('hasPropertyChanged checks', () => {
131
+ expect(propChangedNewInstance.hasPropertyValueChanged('name')).toBe(false);
132
+ propChangedNewInstance.name = 'John Doe';
133
+ expect(propChangedNewInstance.hasPropertyValueChanged('name')).toBe(false);
134
+ propChangedExistingInstance.name = 'John Doe';
135
+ expect(propChangedExistingInstance.hasPropertyValueChanged('name')).toBe(true);
136
+ });
137
+ it("Should throw an error if a property that can't be changed is changed", () => {
138
+ expectErrorMessage(async () => {
139
+ propChangedExistingInstance.age = 40;
140
+ }, 'Property "age" is not allowed to be changed');
141
+ });
142
+ });
143
+
144
+ describe('Constructor checks', () => {
145
+ interface ConstructorBdOrm {
146
+ id: number;
147
+ name: string;
148
+ }
149
+ class ConstructorBdOrm extends BaseORM {
150
+ public static get REQUIRED_PROPERTIES(): string[] {
151
+ return ['name'];
152
+ }
153
+ }
154
+
155
+ it('Should throw an error if a required property is not set', () => {
156
+ expectErrorMessage(async () => {
157
+ new ConstructorBdOrm();
158
+ }, 'Property "name" is required');
159
+ });
160
+
161
+ it('Should set the primary key to 0 on creating a new instance', () => {
162
+ const orm = new ConstructorBdOrm({ name: 'test' });
163
+ expect(orm.id).toBe(0);
164
+ });
165
+ });
166
+ });
@@ -0,0 +1,39 @@
1
+ export default abstract class BdOrmDbConnection {
2
+ /**
3
+ * Create a new entry in the table
4
+ * @param table The table to create the entry in
5
+ * @param data The data to create the entry with
6
+ * @param params Additional parameters
7
+ * @returns The created entry
8
+ */
9
+ public abstract create(
10
+ table: string,
11
+ data: Record<string, any>,
12
+ params?: { primaryKey: string },
13
+ ): Promise<Record<string, any>>;
14
+ public abstract read(
15
+ table: string,
16
+ primaryKeyValue: string | number,
17
+ params?: { primaryKey: string },
18
+ ): Promise<Record<string, any> | undefined>;
19
+ public abstract update(table: string, data: any, params?: { primaryKey: string }): Promise<void>;
20
+ public abstract delete(
21
+ table: string,
22
+ primaryKeyValue: string | number,
23
+ params?: { primaryKey: string },
24
+ ): Promise<void>;
25
+ public abstract list(
26
+ table: string,
27
+ params?: {
28
+ columns?: string | string[];
29
+ filters?: Record<string, any> | string;
30
+ limit?: number;
31
+ offset?: number;
32
+ orderBy?: string;
33
+ values?: any[];
34
+ },
35
+ ): Promise<Record<string, any>[]>;
36
+ public abstract query(query: string, escapedValues?: any[]): Promise<Record<string, any>[]>;
37
+ public abstract getTablePrimaryKey(table: string): Promise<string>;
38
+ public abstract getTableColumns(table: string): Promise<string[]>;
39
+ }
@@ -0,0 +1,18 @@
1
+ import BaseORM from './BdORMBase';
2
+ import cds from '@sap/cds';
3
+
4
+ /**
5
+ * Base class for CDS ORM
6
+ */
7
+ export default abstract class cdsBaseOrm extends BaseORM {
8
+ /**
9
+ * Get the CDS model for the current table
10
+ * @returns the CDS model for the current table
11
+ */
12
+ static get CDS_MODEL(): cds.entity | undefined {
13
+ const namespace = this.TABLE.split('_')[0].toLowerCase();
14
+ return Object.values(cds.entities(namespace)).find(
15
+ (o: any) => 'name' in o && o.name.toUpperCase().replace(/\./g, '_') === this.TABLE,
16
+ );
17
+ }
18
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Use this connection when your project is using SAP CAP and running a SAP HANA DB
3
+ * This class extends the BdOrmDbConnectionSQLBase and provides methods to interact with a SQLite database
4
+ * Need to work with unit tests? use the BdOrmDbConnectionCapSqlite class
5
+ */
6
+ import { BdORMError } from '../../_/BdORMError';
7
+ import BdOrmDbConnectionSQLBase from '../BdOrmDbConnectionSQLBase';
8
+ import { getTableColumns, getTablePrimaryKey } from './utils';
9
+ import cds from '@sap/cds';
10
+
11
+ const PRIMARY_KEYS: Record<string, string> = {};
12
+ const TABLE_COLUMNS: Record<string, string[]> = {};
13
+
14
+ const remapResultColumns = (result: Record<string, any>, columns: string[]) => {
15
+ return Object.entries(result).reduce((resultMapped, [key, value]) => {
16
+ const column = columns.find(column => column.toUpperCase() === key.toUpperCase());
17
+ resultMapped[column ?? key] = value;
18
+ return resultMapped;
19
+ }, {} as Record<string, any>);
20
+ };
21
+
22
+ export default class BdOrmDbConnectionCapHana extends BdOrmDbConnectionSQLBase {
23
+ protected async _db() {
24
+ return cds.db ? cds.db : await cds.connect.to('db');
25
+ }
26
+
27
+ public async close() {
28
+ const db = await this._db();
29
+ //@ts-ignore jest zegt cds.disconnect does not exists
30
+ await db.disconnect();
31
+ }
32
+
33
+ public async getTableColumns(table: string): Promise<string[]> {
34
+ const cdsColumns = getTableColumns(table);
35
+ if (cdsColumns) return cdsColumns;
36
+ if (TABLE_COLUMNS[table]) return TABLE_COLUMNS[table];
37
+ const tableInfo: { COLUMN_NAME: string }[] = await this.query(
38
+ `SELECT COLUMN_NAME FROM TABLE_COLUMNS WHERE TABLE_NAME = '${table}'`,
39
+ );
40
+ if (tableInfo.length === 0) throw new BdORMError(`No columns found for table ${table}`);
41
+ TABLE_COLUMNS[table] = tableInfo.map(info => info.COLUMN_NAME);
42
+ return TABLE_COLUMNS[table];
43
+ }
44
+
45
+ /**
46
+ * Get the primary key of the table
47
+ */
48
+ public async getTablePrimaryKey(table: string): Promise<string> {
49
+ const primaryKey = getTablePrimaryKey(table);
50
+ if (primaryKey) return primaryKey;
51
+ if (PRIMARY_KEYS[table]) return PRIMARY_KEYS[table];
52
+ const tableInfo: [{ COLUMN_NAME: string }] = await this.query(
53
+ `SELECT COLUMN_NAME FROM SYS.INDEX_COLUMNS WHERE TABLE_NAME = '${table}' AND CONSTRAINT = 'PRIMARY KEY'`,
54
+ );
55
+ PRIMARY_KEYS[table] = tableInfo[0]?.COLUMN_NAME;
56
+ if (!PRIMARY_KEYS[table]) throw new BdORMError(`No primary key found for table ${table}`);
57
+ return PRIMARY_KEYS[table];
58
+ }
59
+
60
+ public async create(table: string, data: Record<string, any>) {
61
+ const columns = Object.keys(data),
62
+ placeholders = columns.map(() => '?').join(','),
63
+ pk = await this.getTablePrimaryKey(table);
64
+ await this.query(
65
+ `INSERT INTO ${table} (${columns.join(',')}) VALUES (${placeholders})`,
66
+ columns.map(column => data[column]),
67
+ );
68
+ const result = await this.query(`SELECT * FROM ${table} ORDER BY ${pk} DESC LIMIT 1`);
69
+ return result[0];
70
+ }
71
+
72
+ /**
73
+ * Custom overwrites for list and read > hana stores the db columns in uppercase, so we need to convert the columns to what is defined in the cds file
74
+ */
75
+ public async list(
76
+ table: string,
77
+ options: {
78
+ columns?: string | string[];
79
+ filters?: Record<string, any> | string;
80
+ limit?: number;
81
+ offset?: number;
82
+ orderBy?: string;
83
+ values?: any[];
84
+ } = {},
85
+ ) {
86
+ const results = await super.list(table, options),
87
+ columns = await this.getTableColumns(table);
88
+ return results?.map((result: Record<string, any>) => remapResultColumns(result, columns));
89
+ }
90
+ public async read(table: string, primaryKeyValue: string | number, { primaryKey }: { primaryKey?: string } = {}) {
91
+ const result = await super.read(table, primaryKeyValue, { primaryKey }),
92
+ columns = await this.getTableColumns(table);
93
+ return result ? remapResultColumns(result, columns) : result;
94
+ }
95
+ }
@@ -0,0 +1,47 @@
1
+ import cds from '@sap/cds';
2
+
3
+ type CdsElements = {
4
+ [column: string]: { key?: boolean };
5
+ };
6
+ type Definitions = { [table: string]: { primaryKey: string; columnNames: string[] } };
7
+
8
+ const _definitionCache: Definitions = {} as Definitions;
9
+
10
+ const getDefinitions = () => {
11
+ if (Object.keys(_definitionCache).length) return _definitionCache;
12
+ const cdsDefinitions = (cds?.model?.definitions ?? cds.entities ?? {}) as unknown as {
13
+ [table: string]: {
14
+ name: string;
15
+ elements: CdsElements;
16
+ };
17
+ };
18
+ if (Object.keys(cdsDefinitions).length === 0) return undefined;
19
+ Object.entries(cdsDefinitions).forEach(([key, { elements, name }]) => {
20
+ if (elements === undefined) return;
21
+ (_definitionCache as unknown as Definitions)[name.toLowerCase().replace(/\./g, '_') as unknown as string] = {
22
+ primaryKey: Object.entries(elements).find(([_, { key }]) => key)?.[0] ?? '',
23
+ columnNames: Object.entries(elements).map(([column]) => column),
24
+ };
25
+ });
26
+ return _definitionCache;
27
+ };
28
+
29
+ const getTableDefinition = (table: string) => {
30
+ const definitions = getDefinitions();
31
+ if (!definitions) return undefined;
32
+ const tableDefinition = definitions[table.toLowerCase().replace(/\./g, '_')];
33
+ if (!tableDefinition) return undefined;
34
+ return tableDefinition;
35
+ };
36
+ const getTablePrimaryKey = (table: string) => {
37
+ const tableDefinition = getTableDefinition(table);
38
+ if (!tableDefinition) return undefined;
39
+ return tableDefinition.primaryKey;
40
+ };
41
+ const getTableColumns = (table: string) => {
42
+ const tableDefinition = getTableDefinition(table);
43
+ if (!tableDefinition) return undefined;
44
+ return tableDefinition.columnNames;
45
+ };
46
+
47
+ export { getDefinitions, getTableDefinition, getTablePrimaryKey, getTableColumns };
@@ -0,0 +1,18 @@
1
+ import BdOrmDbConnectionCapSqlite from '../dbConnections/BdOrmDbConnectionCapSqlite';
2
+ import { getDefinitions, getTableColumns, getTablePrimaryKey } from './BdOrmDbConnectionCapHana/utils';
3
+
4
+ describe('BdOrmDbConnectionCapHana', () => {
5
+ let dbConnection: BdOrmDbConnectionCapSqlite;
6
+ //beforeEach(() => {
7
+ dbConnection = new BdOrmDbConnectionCapSqlite(__dirname + '/../test-assets/db');
8
+ //});
9
+
10
+ test('Get the table info', async () => {
11
+ const db = await dbConnection.getDb();
12
+ //console.log(getDefinitions());
13
+ const pk = getTablePrimaryKey('bdorm_user');
14
+ expect(pk).toBe('id');
15
+ const columns = getTableColumns('bdorm_user');
16
+ expect(columns).toEqual(['id', 'firstname', 'lastname', 'email', 'dateCreated', 'options', 'deleted']);
17
+ });
18
+ });