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,288 @@
1
+ import { BdORMError } from './BdORMError';
2
+
3
+ // Local variables
4
+ const PROPERTIES_WEAKMAP = new WeakMap(), // memory leak prevention
5
+ LOCAL_WEAKMAP = new WeakMap();
6
+
7
+ // Helper methods
8
+ const getLocalWeakMapValue = (instance: BdORMBase, key: string) => {
9
+ const localWeakMap = LOCAL_WEAKMAP.get(instance) ?? {};
10
+ return key in localWeakMap ? localWeakMap[key] : null;
11
+ },
12
+ getPropertyDescriptor = (instance: BdORMBase, property: string) => {
13
+ let descriptor;
14
+ while (instance && !descriptor) {
15
+ descriptor = Object.getOwnPropertyDescriptor(instance, property);
16
+ instance = Object.getPrototypeOf(instance);
17
+ }
18
+ return descriptor;
19
+ },
20
+ definePropertyDescriptors = (instance: BdORMBase, data: Record<string, any>) => {
21
+ Object.keys(data).forEach(property => {
22
+ const propertyDefinion = getPropertyDescriptor(instance, property) ?? {};
23
+ if (!('get' in propertyDefinion) || propertyDefinion.get === undefined) {
24
+ propertyDefinion.get = () => {
25
+ const value = instance.getProperty(property) as string;
26
+ if (typeof value === 'string' && (value.trim().startsWith('{') || value.trim().startsWith('['))) {
27
+ try {
28
+ return JSON.parse(value);
29
+ } catch (e) {
30
+ return value;
31
+ }
32
+ }
33
+ return value;
34
+ };
35
+ }
36
+ if (!('set' in propertyDefinion) || propertyDefinion.set === undefined) {
37
+ propertyDefinion.set = (value: any) => {
38
+ if (value && typeof value === 'object') {
39
+ try {
40
+ instance.setProperty(property, JSON.stringify(value));
41
+ } catch (e) {
42
+ instance.setProperty(property, value);
43
+ }
44
+ return;
45
+ }
46
+ instance.setProperty(property, value);
47
+ };
48
+ }
49
+ if ('value' in propertyDefinion && propertyDefinion.value === undefined) {
50
+ delete propertyDefinion.value;
51
+ if ('writable' in propertyDefinion) delete propertyDefinion.writable;
52
+ }
53
+ try {
54
+ Object.defineProperty(instance, property, propertyDefinion);
55
+ } catch (e) {
56
+ throw new BdORMError(
57
+ `Invalid property descriptor "${property}" ${propertyDefinion.value} in ${instance.constructor.name}`,
58
+ );
59
+ }
60
+ // data toevoegen
61
+ if (data[property] !== undefined) instance[property] = data[property];
62
+ });
63
+ };
64
+
65
+ /**
66
+ * Class which only contains the getters and setters for the base ORM
67
+ */
68
+ abstract class BdORMBase {
69
+ /**
70
+ * Properties will be defined in the constructor (see definePropertyDescriptors)
71
+ */
72
+ [key: string]: any;
73
+
74
+ /**
75
+ * @returns the model name
76
+ */
77
+ public static getModelName(): string {
78
+ return this.name;
79
+ }
80
+
81
+ constructor(data: Record<string, any> = {}) {
82
+ const ormData = { ...data };
83
+ // primary key to 0 if not present
84
+ const primaryKey = this.getPrimaryKey();
85
+ if (!(primaryKey in ormData)) {
86
+ ormData[primaryKey] = 0; // 0 means new entry
87
+ }
88
+ // required property check
89
+ if (this.getRequiredProperties().length > 0) {
90
+ this.getRequiredProperties().forEach(property => {
91
+ if (!(property in ormData)) throw new BdORMError(`Property "${property}" is required`);
92
+ });
93
+ }
94
+ // set properties
95
+ definePropertyDescriptors(this, ormData);
96
+ }
97
+
98
+ /**
99
+ * This method is ONLY used in the FETCH method
100
+ * @returns the columns to fetch from the database
101
+ */
102
+ public static readonly COLUMNS_TO_FETCH: string | string[] = '*';
103
+
104
+ /**
105
+ * which default methods are not allowed to be used
106
+ * for example: ['delete'] will prevent you from using model.delete();
107
+ */
108
+ public static readonly METHODS_NOT_ALLOWED: string[] = [];
109
+
110
+ /**
111
+ * Get the primary key for the current table
112
+ * @returns the primary key for the current table
113
+ */
114
+ public static readonly PRIMARY_KEY: string = 'id';
115
+
116
+ /**
117
+ * Specify which properties should be set as read-only.
118
+ * These properties can only be set once, when the object is created.
119
+ * @return array
120
+ */
121
+ public static readonly PROPERTIES_NOT_ALLOWED_TO_CHANGE: string[] = [];
122
+
123
+ /**
124
+ * If there are values that should not be duplicated,
125
+ * specify here which properties those should be.
126
+ * @return array
127
+ */
128
+ public static readonly PROPERTIES_NOT_ALLOWED_TO_DUPLICATE: string[] = [];
129
+
130
+ /**
131
+ * Specify which properties are required when creating a new object.
132
+ * @return array
133
+ */
134
+ public static readonly REQUIRED_PROPERTIES: string[] = [];
135
+
136
+ public static isMethodAllowed(method: string, { throwErrorIfNotAllowed = false } = {}): boolean {
137
+ const allowed = !this.METHODS_NOT_ALLOWED.includes(method);
138
+ if (!allowed && throwErrorIfNotAllowed) {
139
+ throw new BdORMError(`Method "${method}" not allowed`);
140
+ }
141
+ return allowed;
142
+ }
143
+
144
+ /**
145
+ * Get the table name as used in the database
146
+ * @returns the table name as used in the database
147
+ */
148
+ public static readonly DB_TABLE: string;
149
+ public static get TABLE(): string {
150
+ if (!this.DB_TABLE) throw BdORMError.staticNotImplemented('get TABLE()');
151
+ return this.DB_TABLE;
152
+ }
153
+
154
+ /**
155
+ * Sometimes you want to use a custom view to read your model data
156
+ * @returns the view name as used in the database
157
+ */
158
+ public static readonly DB_VIEW: string;
159
+ public static get VIEW(): string {
160
+ return this.DB_VIEW ?? this.TABLE;
161
+ }
162
+
163
+ protected _setChangedProperties(changedProperties: string[]): void {
164
+ const local = LOCAL_WEAKMAP.get(this) || {};
165
+ LOCAL_WEAKMAP.set(this, {
166
+ ...local,
167
+ changedProperties,
168
+ });
169
+ }
170
+
171
+ /**
172
+ * @returns the changed properties
173
+ */
174
+ public getChangedProperties(): string[] {
175
+ return !this.isNew() ? (getLocalWeakMapValue(this, 'changedProperties') ?? []) : [];
176
+ }
177
+
178
+ /**
179
+ * @returns the data in json format
180
+ * @paaam {boolean} [preventFormatting=false] - prevent formatting of the data
181
+ */
182
+ getData(preventFormatting = false): Record<string, any> {
183
+ const data: Record<string, any> = {},
184
+ returnData: Record<string, any> = {};
185
+ const properties = PROPERTIES_WEAKMAP.get(this) ?? {};
186
+ Object.keys(properties).forEach(property => {
187
+ data[property] = this.getProperty(property, { preventFormatting });
188
+ returnData[property] = preventFormatting ? data[property] : (this[property] ?? data[property]);
189
+ });
190
+ return returnData;
191
+ }
192
+
193
+ /**
194
+ * @returns the primary key for the current table
195
+ */
196
+ public getPrimaryKey(): string {
197
+ return (this.constructor as typeof BdORMBase).PRIMARY_KEY;
198
+ }
199
+
200
+ public getPrimaryKeyValue(): number {
201
+ return this.getProperty(this.getPrimaryKey()) as number;
202
+ }
203
+
204
+ /**
205
+ * get the model property
206
+ */
207
+ public getProperty(key: string, { preventFormatting = false } = {}): string | number | null | undefined {
208
+ const properties = PROPERTIES_WEAKMAP.get(this) ?? {};
209
+ let value = key in properties ? properties[key] : null; // never properties[key] as it can be set to null, undefined or 0
210
+ if (!preventFormatting && value instanceof Buffer) value = value.toString();
211
+ return value;
212
+ }
213
+
214
+ /**
215
+ * @returns the columns of the required properties needed to create a new object
216
+ */
217
+ public getRequiredProperties(): string[] {
218
+ return (this.constructor as typeof BdORMBase).REQUIRED_PROPERTIES;
219
+ }
220
+
221
+ /**
222
+ * Get the table name as used in the database
223
+ * @returns the table name as used in the database
224
+ */
225
+ public getTable(): string {
226
+ return (this.constructor as typeof BdORMBase).TABLE;
227
+ }
228
+
229
+ /**
230
+ *
231
+ * @returns
232
+ */
233
+ public hasChanges(): boolean {
234
+ return this.getChangedProperties().length > 0;
235
+ }
236
+
237
+ /**
238
+ * Check if the property has been changed
239
+ */
240
+ public hasPropertyValueChanged(property: string): boolean {
241
+ return this.getChangedProperties().includes(property);
242
+ }
243
+
244
+ /**
245
+ * Check if the current object is a new object
246
+ * @returns true if the object is new, false if it is not
247
+ */
248
+ public isNew(): boolean {
249
+ return !this.getPrimaryKeyValue();
250
+ }
251
+
252
+ public setProperty(property: string, value: any, { forceChange = false } = {}): void {
253
+ // prevent overwrite of primary key
254
+ if (property === this.getPrimaryKey() && this.getPrimaryKeyValue() && this.getPrimaryKeyValue() !== value) {
255
+ throw new BdORMError(`Primary key cannot be changed from ${this.getPrimaryKeyValue()} to ${value}`);
256
+ }
257
+ // prevent overwrite of read-only properties
258
+ if (
259
+ !forceChange &&
260
+ !this.isNew() &&
261
+ (this.constructor as typeof BdORMBase).PROPERTIES_NOT_ALLOWED_TO_CHANGE.includes(property) &&
262
+ ![undefined, null].includes(this.getProperty(property) as null) &&
263
+ this.getProperty(property) != value // != is used to check for null and undefined
264
+ ) {
265
+ throw new BdORMError(`Property "${property}" is not allowed to be changed`);
266
+ }
267
+ if (!this.isNew() && this.getPrimaryKey() === property && this.getPrimaryKeyValue() !== value) {
268
+ throw new BdORMError(`Primary key "${property}" is not allowed to be changed`);
269
+ }
270
+ // set changed flag
271
+ const origValue = this.getProperty(property);
272
+ if (![undefined, null].includes(this.getProperty(property) as null) && origValue !== value) {
273
+ // todo set orig
274
+ const changedProperties = this.getChangedProperties();
275
+ if (!changedProperties.includes(property)) {
276
+ changedProperties.push(property);
277
+ this._setChangedProperties(changedProperties);
278
+ }
279
+ }
280
+ // waarde instellen
281
+ const properties = PROPERTIES_WEAKMAP.get(this) ?? {};
282
+ properties[property] = value;
283
+ PROPERTIES_WEAKMAP.set(this, properties);
284
+ }
285
+ }
286
+
287
+ export default BdORMBase;
288
+ export { definePropertyDescriptors };
@@ -0,0 +1,198 @@
1
+ import BdOrmDbConnectionCapSqlite from '../dbConnections/BdOrmDbConnectionCapSqlite';
2
+ import BdOrm from '../index';
3
+ import User from '../test-assets/User';
4
+ import { expectErrorMessage } from '../test-assets/jestUtils';
5
+ import cds from '@sap/cds';
6
+
7
+ describe('bd-orm BdORMCrud', () => {
8
+ let dbConnection: BdOrmDbConnectionCapSqlite;
9
+ //beforeEach(() => {
10
+ BdOrm.clearDbConnection();
11
+ dbConnection = new BdOrmDbConnectionCapSqlite(__dirname + '/../test-assets/db');
12
+ BdOrm.setDbConnection(dbConnection);
13
+ //});
14
+
15
+ it('Prevent setting the db connection twice', () => {
16
+ expectErrorMessage(async () => BdOrm.setDbConnection(dbConnection), 'DbConnection already set');
17
+ });
18
+
19
+ it("Count the number of users in the database, it's 2", async () => {
20
+ const count = await User.count();
21
+ expect(count).toBe(2);
22
+ });
23
+
24
+ it('Create a user ', async () => {
25
+ const user = new User({ firstname: 'Test', lastname: 'Persoon' });
26
+ await user.save();
27
+ expect(user.id).toBe(3);
28
+ const results = await dbConnection.query('SELECT * FROM bdorm_user WHERE id = 3');
29
+ expect(results[0].firstname).toBe('Test');
30
+ expect(results[0].lastname).toBe('Persoon');
31
+ // alternate creation method
32
+ const user2 = await User.create({ firstname: 'Test2', lastname: 'Persoon2' });
33
+ expect(user2.id).toBe(4);
34
+ const results2 = await dbConnection.query('SELECT * FROM bdorm_user WHERE id = 4');
35
+ expect(results2[0].firstname).toBe('Test2');
36
+ expect(results2[0].lastname).toBe('Persoon2');
37
+ });
38
+
39
+ it('Create a user with properties that are not in the table', async () => {
40
+ const user = new User({ firstname: 'Invalid', lastname: 'Property', invalidprop: 'sadf' });
41
+ await user.save();
42
+ const results = await dbConnection.query('SELECT * FROM bdorm_user WHERE id = ?', [user.id]);
43
+ expect(results[0].firstname).toBe('Invalid');
44
+ expect(results[0].lastname).toBe('Property');
45
+ });
46
+
47
+ it('Delete a user', async () => {
48
+ // first add a user
49
+ const user = await User.create({ firstname: 'Delete', lastname: 'Me' });
50
+ const id = user.id;
51
+ const results = await dbConnection.query('SELECT * FROM bdorm_user WHERE id = ?', [id]);
52
+ expect(results).toHaveLength(1);
53
+ // then delete the user
54
+ await user.delete();
55
+ const resultsAfterDelete = await dbConnection.query('SELECT * FROM bdorm_user WHERE id = ?', [id]);
56
+ expect(resultsAfterDelete).toHaveLength(0);
57
+ const resultsAfterDelete2 = await dbConnection.query('SELECT * FROM bdorm_user WHERE firstname = ?', [
58
+ 'Delete',
59
+ ]);
60
+ expect(resultsAfterDelete2).toHaveLength(0);
61
+ });
62
+
63
+ it('Fetch a user by id', async () => {
64
+ const user = await User.fetchByPrimaryKey(1);
65
+ expect(user).toBeInstanceOf(User);
66
+ expect(user?.firstname).toBe('John');
67
+ expect(user?.lastname).toBe('Doe');
68
+
69
+ const user2 = await User.fetchById(2);
70
+ expect(user2).toBeInstanceOf(User);
71
+ expect(user2?.firstname).toBe('Barry');
72
+ expect(user2?.lastname).toBe('Dam');
73
+ });
74
+
75
+ it('Fetch multiple users', async () => {
76
+ const users = await User.fetch();
77
+ expect(users[0]).toBeInstanceOf(User);
78
+ expect(users[1]).toBeInstanceOf(User);
79
+ expect(users[0].firstname).toBe('John');
80
+ expect(users[1].firstname).toBe('Barry');
81
+ });
82
+
83
+ it('Fetch with a where clause as object', async () => {
84
+ const users = await User.fetch({ firstname: 'John' });
85
+ expect(users).toHaveLength(1);
86
+ expect(users[0].firstname).toBe('John');
87
+ expect(users[0].lastname).toBe('Doe');
88
+ });
89
+
90
+ it('Fetch with a where clause as string', async () => {
91
+ const users = await User.fetch('firstname = ?', ['Barry']);
92
+ expect(users).toHaveLength(1);
93
+ expect(users[0].firstname).toBe('Barry');
94
+ expect(users[0].lastname).toBe('Dam');
95
+ });
96
+
97
+ it("Fetch where clause that doesn't match", async () => {
98
+ const user = await User.fetch({ firstname: 'Barry', lastname: 'Doe' });
99
+ expect(user.length).toBe(0);
100
+ });
101
+
102
+ it('Fetch One', async () => {
103
+ const user = await User.fetchOne({ firstname: 'Barry' });
104
+ expect(user).toBeInstanceOf(User);
105
+ expect(user?.firstname).toBe('Barry');
106
+ expect(user?.lastname).toBe('Dam');
107
+ });
108
+
109
+ it('Update a user', async () => {
110
+ const user = await User.fetchById(1);
111
+ expect(user).toBeInstanceOf(User);
112
+ if (!user) throw new Error('User not found');
113
+ const firstname = user.firstname;
114
+ user.firstname = 'Jane';
115
+ await user.save();
116
+ const results = await dbConnection.query('SELECT * FROM bdorm_user WHERE id = 1');
117
+ expect(results[0].firstname).toBe('Jane');
118
+ user.firstname = firstname;
119
+ await user.save();
120
+ const results2 = await dbConnection.query('SELECT * FROM bdorm_user WHERE id = 1');
121
+ expect(results2[0].firstname).toBe('John');
122
+ });
123
+
124
+ it('Should save JSON options as a string in the db', async () => {
125
+ const user = new User({ firstname: 'Json', lastname: 'Persoon', options: { test: 'test 123' } });
126
+ await user.save();
127
+ const results = await dbConnection.query('SELECT * FROM bdorm_user WHERE firstname = ?', ['Json']);
128
+ expect(results[0].options).toBe('{"test":"test 123"}');
129
+ const userJSON = await User.fetchOne({ firstname: 'Json' });
130
+ expect(userJSON?.options).toEqual({ test: 'test 123' });
131
+ expect(userJSON?.options?.test).toEqual('test 123');
132
+ });
133
+
134
+ it('Should not allow me to run delete and save', async () => {
135
+ class userNotAllowedDeleteAndSave extends User {
136
+ static readonly METHODS_NOT_ALLOWED = ['save', 'delete'];
137
+ }
138
+ const user = new userNotAllowedDeleteAndSave({ firstname: 'Json', lastname: 'Persoon' });
139
+ expectErrorMessage(async () => {
140
+ await user.save();
141
+ }, 'Method "save" not allowed');
142
+ expectErrorMessage(async () => {
143
+ await user.delete();
144
+ }, 'Method "delete" not allowed');
145
+ });
146
+
147
+ it('Columns to fetch is altered and works correctly', async () => {
148
+ class UserWithColumnsToFetch extends User {
149
+ static readonly COLUMNS_TO_FETCH = ['id', 'firstname'];
150
+ }
151
+ const users = await UserWithColumnsToFetch.fetch();
152
+ expect(users[0]).toBeInstanceOf(UserWithColumnsToFetch);
153
+ expect(users[0].firstname).toBe('John');
154
+ expect(users[0].lastname).toBeUndefined();
155
+ class UserWithColumnsToFetch2 extends User {
156
+ static readonly COLUMNS_TO_FETCH = 'id, firstname, lastname as achternaam';
157
+ }
158
+ const user = await UserWithColumnsToFetch2.fetchOne('firstname = ? and lastname = ?', ['John', 'Doe']);
159
+ expect(user).toBeInstanceOf(UserWithColumnsToFetch2);
160
+ expect(user?.firstname).toBe('John');
161
+ expect(user?.lastname).toBeUndefined();
162
+ expect(user?.achternaam).toBe('Doe');
163
+ expect(user?.fullname).toBeUndefined();
164
+ });
165
+
166
+ describe('Duplicate tests', () => {
167
+ it('Should duplicate the user as draft and remove dateCreated', async () => {
168
+ const user = await User.fetchById(1); // John Doe
169
+ const user2 = await user?.duplicate({ asDraft: true });
170
+ expect(user2).toBeInstanceOf(User);
171
+ expect(user2?.firstname).toBe('John');
172
+ expect(user2?.lastname).toBe('Doe');
173
+ expect(user2?.isNew()).toBe(true);
174
+ expect(user2?.dateCreated).toBeUndefined();
175
+ });
176
+ it('Should duplicate the user as draft and set the firstname to Johnny', async () => {
177
+ const user = await User.fetchById(1); // John Doe
178
+ const user2 = await user?.duplicate({ asDraft: true, newData: { firstname: 'Johnny' } });
179
+ expect(user2).toBeInstanceOf(User);
180
+ expect(user2?.firstname).toBe('Johnny');
181
+ expect(user2?.lastname).toBe('Doe');
182
+ expect(user2?.isNew()).toBe(true);
183
+ });
184
+ it('Should duplicate the user and save it to the DB', async () => {
185
+ const user = await User.fetchById(1); // John Doe
186
+ const user2 = await user?.duplicate();
187
+ expect(user2).toBeInstanceOf(User);
188
+ expect(user2?.firstname).toBe('John');
189
+ expect(user2?.lastname).toBe('Doe');
190
+ expect(user2?.isNew()).toBe(false);
191
+ expect(user2?.dateCreated).not.toBeUndefined();
192
+ const results = await dbConnection.query('SELECT * FROM bdorm_user WHERE id = ?', [user2?.id]);
193
+ expect(results[0].firstname).toBe('John');
194
+ expect(results[0].lastname).toBe('Doe');
195
+ expect(results[0].id).toBe(user2?.id);
196
+ });
197
+ });
198
+ });