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.
- package/.github/workflows/build-and-test.yml +26 -0
- package/.github/workflows/publish.yml +36 -0
- package/.prettierrc.json +14 -0
- package/CHANGELOG.md +3 -0
- package/README.md +5 -0
- package/check NBR en TODO.txt +32 -0
- package/dist/_/BdORMBase.d.ts +111 -0
- package/dist/_/BdORMBase.js +262 -0
- package/dist/_/BdORMCrud.d.ts +51 -0
- package/dist/_/BdORMCrud.js +223 -0
- package/dist/_/BdORMError.d.ts +4 -0
- package/dist/_/BdORMError.js +14 -0
- package/dist/_/BdOrmConnection.d.ts +32 -0
- package/dist/_/BdOrmConnection.js +5 -0
- package/dist/_/cdsBaseOrm.d.ts +12 -0
- package/dist/_/cdsBaseOrm.js +21 -0
- package/dist/dbConnections/BdOrmDbConnectionCapHana/index.d.ts +26 -0
- package/dist/dbConnections/BdOrmDbConnectionCapHana/index.js +78 -0
- package/dist/dbConnections/BdOrmDbConnectionCapHana/utils.d.ts +14 -0
- package/dist/dbConnections/BdOrmDbConnectionCapHana/utils.js +49 -0
- package/dist/dbConnections/BdOrmDbConnectionCapSqlite/index.d.ts +22 -0
- package/dist/dbConnections/BdOrmDbConnectionCapSqlite/index.js +85 -0
- package/dist/dbConnections/BdOrmDbConnectionCapSqlite/utils.d.ts +3 -0
- package/dist/dbConnections/BdOrmDbConnectionCapSqlite/utils.js +74 -0
- package/dist/dbConnections/BdOrmDbConnectionSQLBase.d.ts +32 -0
- package/dist/dbConnections/BdOrmDbConnectionSQLBase.js +99 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +18 -0
- package/dist/test-assets/Post.d.ts +12 -0
- package/dist/test-assets/Post.js +13 -0
- package/dist/test-assets/User.d.ts +16 -0
- package/dist/test-assets/User.js +13 -0
- package/dist/test-assets/jestUtils.d.ts +5 -0
- package/dist/test-assets/jestUtils.js +30 -0
- package/jest.config.js +7 -0
- package/package.json +53 -0
- package/src/_/BdORMBase.ts +288 -0
- package/src/_/BdORMCrud.test.ts +198 -0
- package/src/_/BdORMCrud.ts +240 -0
- package/src/_/BdORMError.ts +10 -0
- package/src/_/BdORMbase.test.ts +166 -0
- package/src/_/BdOrmConnection.ts +39 -0
- package/src/_/cdsBaseOrm.ts +18 -0
- package/src/dbConnections/BdOrmDbConnectionCapHana/index.ts +95 -0
- package/src/dbConnections/BdOrmDbConnectionCapHana/utils.ts +47 -0
- package/src/dbConnections/BdOrmDbConnectionCapHana.test.ts +18 -0
- package/src/dbConnections/BdOrmDbConnectionCapSqlite/index.ts +89 -0
- package/src/dbConnections/BdOrmDbConnectionCapSqlite/utils.ts +75 -0
- package/src/dbConnections/BdOrmDbConnectionCapSqlite.test.ts +41 -0
- package/src/dbConnections/BdOrmDbConnectionSQLBase.ts +124 -0
- package/src/index.ts +8 -0
- package/src/test-assets/Post.ts +16 -0
- package/src/test-assets/User.ts +18 -0
- package/src/test-assets/db/BdOrm.cds +20 -0
- package/src/test-assets/db/csv/bdorm-user.csv +3 -0
- package/src/test-assets/db/views/user.hdbview +3 -0
- package/src/test-assets/jestUtils.ts +24 -0
- package/tsconfig.json +16 -0
- 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
|
+
});
|