@proteinjs/db 1.0.2

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 (165) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/dist/generated/index.d.ts +11 -0
  4. package/dist/generated/index.d.ts.map +1 -0
  5. package/dist/generated/index.js +59 -0
  6. package/dist/generated/index.js.map +1 -0
  7. package/dist/index.d.ts +21 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +37 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/src/Columns.d.ts +100 -0
  12. package/dist/src/Columns.d.ts.map +1 -0
  13. package/dist/src/Columns.js +381 -0
  14. package/dist/src/Columns.js.map +1 -0
  15. package/dist/src/Db.d.ts +46 -0
  16. package/dist/src/Db.d.ts.map +1 -0
  17. package/dist/src/Db.js +393 -0
  18. package/dist/src/Db.js.map +1 -0
  19. package/dist/src/MigrationRunner.d.ts +10 -0
  20. package/dist/src/MigrationRunner.d.ts.map +1 -0
  21. package/dist/src/MigrationRunner.js +124 -0
  22. package/dist/src/MigrationRunner.js.map +1 -0
  23. package/dist/src/QueryBuilderFactory.d.ts +8 -0
  24. package/dist/src/QueryBuilderFactory.d.ts.map +1 -0
  25. package/dist/src/QueryBuilderFactory.js +21 -0
  26. package/dist/src/QueryBuilderFactory.js.map +1 -0
  27. package/dist/src/Record.d.ts +38 -0
  28. package/dist/src/Record.d.ts.map +1 -0
  29. package/dist/src/Record.js +209 -0
  30. package/dist/src/Record.js.map +1 -0
  31. package/dist/src/RecordIterator.d.ts +13 -0
  32. package/dist/src/RecordIterator.d.ts.map +1 -0
  33. package/dist/src/RecordIterator.js +98 -0
  34. package/dist/src/RecordIterator.js.map +1 -0
  35. package/dist/src/StatementConfigFactory.d.ts +11 -0
  36. package/dist/src/StatementConfigFactory.d.ts.map +1 -0
  37. package/dist/src/StatementConfigFactory.js +31 -0
  38. package/dist/src/StatementConfigFactory.js.map +1 -0
  39. package/dist/src/Table.d.ts +98 -0
  40. package/dist/src/Table.d.ts.map +1 -0
  41. package/dist/src/Table.js +56 -0
  42. package/dist/src/Table.js.map +1 -0
  43. package/dist/src/auth/TableAuth.d.ts +29 -0
  44. package/dist/src/auth/TableAuth.d.ts.map +1 -0
  45. package/dist/src/auth/TableAuth.js +45 -0
  46. package/dist/src/auth/TableAuth.js.map +1 -0
  47. package/dist/src/auth/TableServiceAuth.d.ts +5 -0
  48. package/dist/src/auth/TableServiceAuth.d.ts.map +1 -0
  49. package/dist/src/auth/TableServiceAuth.js +37 -0
  50. package/dist/src/auth/TableServiceAuth.js.map +1 -0
  51. package/dist/src/opt/moment.d.ts +4 -0
  52. package/dist/src/opt/moment.d.ts.map +1 -0
  53. package/dist/src/opt/moment.js +9 -0
  54. package/dist/src/opt/moment.js.map +1 -0
  55. package/dist/src/reference/Reference.d.ts +26 -0
  56. package/dist/src/reference/Reference.d.ts.map +1 -0
  57. package/dist/src/reference/Reference.js +90 -0
  58. package/dist/src/reference/Reference.js.map +1 -0
  59. package/dist/src/reference/ReferenceArray.d.ts +29 -0
  60. package/dist/src/reference/ReferenceArray.d.ts.map +1 -0
  61. package/dist/src/reference/ReferenceArray.js +101 -0
  62. package/dist/src/reference/ReferenceArray.js.map +1 -0
  63. package/dist/src/schema/SchemaMetadata.d.ts +27 -0
  64. package/dist/src/schema/SchemaMetadata.d.ts.map +1 -0
  65. package/dist/src/schema/SchemaMetadata.js +227 -0
  66. package/dist/src/schema/SchemaMetadata.js.map +1 -0
  67. package/dist/src/schema/SchemaOperations.d.ts +40 -0
  68. package/dist/src/schema/SchemaOperations.d.ts.map +1 -0
  69. package/dist/src/schema/SchemaOperations.js +3 -0
  70. package/dist/src/schema/SchemaOperations.js.map +1 -0
  71. package/dist/src/schema/TableManager.d.ts +21 -0
  72. package/dist/src/schema/TableManager.d.ts.map +1 -0
  73. package/dist/src/schema/TableManager.js +254 -0
  74. package/dist/src/schema/TableManager.js.map +1 -0
  75. package/dist/src/serializers/GraphSerializer.d.ts +13 -0
  76. package/dist/src/serializers/GraphSerializer.d.ts.map +1 -0
  77. package/dist/src/serializers/GraphSerializer.js +21 -0
  78. package/dist/src/serializers/GraphSerializer.js.map +1 -0
  79. package/dist/src/serializers/QueryBuilderSerializer.d.ts +19 -0
  80. package/dist/src/serializers/QueryBuilderSerializer.d.ts.map +1 -0
  81. package/dist/src/serializers/QueryBuilderSerializer.js +32 -0
  82. package/dist/src/serializers/QueryBuilderSerializer.js.map +1 -0
  83. package/dist/src/serializers/ReferenceArraySerializer.d.ts +15 -0
  84. package/dist/src/serializers/ReferenceArraySerializer.d.ts.map +1 -0
  85. package/dist/src/serializers/ReferenceArraySerializer.js +23 -0
  86. package/dist/src/serializers/ReferenceArraySerializer.js.map +1 -0
  87. package/dist/src/serializers/ReferenceSerializer.d.ts +15 -0
  88. package/dist/src/serializers/ReferenceSerializer.d.ts.map +1 -0
  89. package/dist/src/serializers/ReferenceSerializer.js +23 -0
  90. package/dist/src/serializers/ReferenceSerializer.js.map +1 -0
  91. package/dist/src/serializers/TableSerializer.d.ts +13 -0
  92. package/dist/src/serializers/TableSerializer.d.ts.map +1 -0
  93. package/dist/src/serializers/TableSerializer.js +19 -0
  94. package/dist/src/serializers/TableSerializer.js.map +1 -0
  95. package/dist/src/services/DbService.d.ts +19 -0
  96. package/dist/src/services/DbService.d.ts.map +1 -0
  97. package/dist/src/services/DbService.js +6 -0
  98. package/dist/src/services/DbService.js.map +1 -0
  99. package/dist/src/services/MigrationRunnerService.d.ts +6 -0
  100. package/dist/src/services/MigrationRunnerService.d.ts.map +1 -0
  101. package/dist/src/services/MigrationRunnerService.js +6 -0
  102. package/dist/src/services/MigrationRunnerService.js.map +1 -0
  103. package/dist/src/source/SourceRecord.d.ts +44 -0
  104. package/dist/src/source/SourceRecord.d.ts.map +1 -0
  105. package/dist/src/source/SourceRecord.js +49 -0
  106. package/dist/src/source/SourceRecord.js.map +1 -0
  107. package/dist/src/source/SourceRecordLoader.d.ts +6 -0
  108. package/dist/src/source/SourceRecordLoader.d.ts.map +1 -0
  109. package/dist/src/source/SourceRecordLoader.js +156 -0
  110. package/dist/src/source/SourceRecordLoader.js.map +1 -0
  111. package/dist/src/source/SourceRecordRepo.d.ts +8 -0
  112. package/dist/src/source/SourceRecordRepo.d.ts.map +1 -0
  113. package/dist/src/source/SourceRecordRepo.js +20 -0
  114. package/dist/src/source/SourceRecordRepo.js.map +1 -0
  115. package/dist/src/tables/MigrationTable.d.ts +41 -0
  116. package/dist/src/tables/MigrationTable.d.ts.map +1 -0
  117. package/dist/src/tables/MigrationTable.js +86 -0
  118. package/dist/src/tables/MigrationTable.js.map +1 -0
  119. package/dist/src/tables/tables.d.ts +6 -0
  120. package/dist/src/tables/tables.d.ts.map +1 -0
  121. package/dist/src/tables/tables.js +8 -0
  122. package/dist/src/tables/tables.js.map +1 -0
  123. package/dist/test/CrudTests.d.ts +20 -0
  124. package/dist/test/CrudTests.d.ts.map +1 -0
  125. package/dist/test/CrudTests.js +251 -0
  126. package/dist/test/CrudTests.js.map +1 -0
  127. package/dist/test/TableManagerTests.d.ts +8 -0
  128. package/dist/test/TableManagerTests.d.ts.map +1 -0
  129. package/dist/test/TableManagerTests.js +687 -0
  130. package/dist/test/TableManagerTests.js.map +1 -0
  131. package/generated/index.ts +55 -0
  132. package/index.ts +25 -0
  133. package/jest.config.js +18 -0
  134. package/package.json +45 -0
  135. package/src/Columns.ts +257 -0
  136. package/src/Db.ts +195 -0
  137. package/src/MigrationRunner.ts +62 -0
  138. package/src/QueryBuilderFactory.ts +18 -0
  139. package/src/Record.ts +113 -0
  140. package/src/RecordIterator.ts +44 -0
  141. package/src/StatementConfigFactory.ts +36 -0
  142. package/src/Table.ts +124 -0
  143. package/src/auth/TableAuth.ts +60 -0
  144. package/src/auth/TableServiceAuth.ts +32 -0
  145. package/src/opt/moment.ts +4 -0
  146. package/src/reference/Reference.ts +45 -0
  147. package/src/reference/ReferenceArray.ts +56 -0
  148. package/src/schema/SchemaMetadata.ts +115 -0
  149. package/src/schema/SchemaOperations.ts +44 -0
  150. package/src/schema/TableManager.ts +198 -0
  151. package/src/serializers/GraphSerializer.ts +22 -0
  152. package/src/serializers/QueryBuilderSerializer.ts +39 -0
  153. package/src/serializers/ReferenceArraySerializer.ts +26 -0
  154. package/src/serializers/ReferenceSerializer.ts +26 -0
  155. package/src/serializers/TableSerializer.ts +20 -0
  156. package/src/services/DbService.ts +19 -0
  157. package/src/services/MigrationRunnerService.ts +7 -0
  158. package/src/source/SourceRecord.ts +75 -0
  159. package/src/source/SourceRecordLoader.ts +68 -0
  160. package/src/source/SourceRecordRepo.ts +17 -0
  161. package/src/tables/MigrationTable.ts +36 -0
  162. package/src/tables/tables.ts +6 -0
  163. package/test/CrudTests.ts +110 -0
  164. package/test/TableManagerTests.ts +382 -0
  165. package/tsconfig.json +23 -0
package/src/Db.ts ADDED
@@ -0,0 +1,195 @@
1
+ import { DbService, Query, getDbService } from './services/DbService';
2
+ import { Service } from '@proteinjs/service';
3
+ import { Loadable, SourceRepository } from '@proteinjs/reflection';
4
+ import { Column, Table, getColumnPropertyName, tableByName } from './Table';
5
+ import { Record, RecordSerializer, SerializedRecord } from './Record';
6
+ import { Logger } from '@proteinjs/util';
7
+ import { SourceRecordLoader } from './source/SourceRecordLoader';
8
+ import { ParameterizationConfig, QueryBuilder, Statement, StatementFactory } from '@proteinjs/db-query';
9
+ import { QueryBuilderFactory } from './QueryBuilderFactory';
10
+ import { StatementConfigFactory } from './StatementConfigFactory';
11
+ import { TableManager } from './schema/TableManager';
12
+ import { TableAuth } from './auth/TableAuth';
13
+ import { TableServiceAuth } from './auth/TableServiceAuth';
14
+
15
+ export const getDb = <R extends Record = Record>() => typeof self === 'undefined' ? new Db<R>() : getDbService() as Db<R>;
16
+ export const getSystemDb = <R extends Record = Record>() => new Db<R>(undefined, undefined, true);
17
+
18
+ export type DbDriverStatementConfig = ParameterizationConfig & { prefixTablesWithDb?: boolean }
19
+
20
+ export interface DbDriver extends Loadable {
21
+ getDbName(): string;
22
+ createDbIfNotExists(): Promise<void>;
23
+ start?(): Promise<void>;
24
+ stop?(): Promise<void>;
25
+ getTableManager(): TableManager;
26
+ runQuery(generateStatement: (config: DbDriverStatementConfig) => Statement): Promise<SerializedRecord[]>;
27
+ runDml(generateStatement: (config: DbDriverStatementConfig) => Statement): Promise<number>; // returns the number of affected rows
28
+ }
29
+
30
+ export class Db<R extends Record = Record> implements DbService<R> {
31
+ private static globalDbDriver: DbDriver;
32
+ private dbDriver: DbDriver;
33
+ private logger = new Logger(this.constructor.name);
34
+ private statementConfigFactory: StatementConfigFactory;
35
+ private auth = new TableAuth();
36
+ public serviceMetadata: Service['serviceMetadata'] = {
37
+ auth: {
38
+ canAccess: (methodName, args) => new TableServiceAuth().canAccess(methodName, args),
39
+ },
40
+ };
41
+
42
+ constructor(
43
+ dbDriver?: DbDriver,
44
+ getTable?: (tableName: string) => Table<any>,
45
+ private ignoreAuth?: boolean
46
+ ) {
47
+ this.dbDriver = dbDriver ? dbDriver : this.getGlobalDbDriver();
48
+ this.statementConfigFactory = new StatementConfigFactory(this.dbDriver.getDbName(), getTable);
49
+ }
50
+
51
+ private getGlobalDbDriver(): DbDriver {
52
+ if (!Db.globalDbDriver) {
53
+ Db.globalDbDriver = SourceRepository.get().object<DbDriver>('@proteinjs/db/DbDriver');
54
+ if (!Db.globalDbDriver)
55
+ throw new Error(`Unable to find @proteinjs/db/DbDriver implementation. Make sure you've included one as a package dependency.`);
56
+ }
57
+
58
+ return Db.globalDbDriver;
59
+ }
60
+
61
+ async init(): Promise<void> {
62
+ await this.dbDriver.createDbIfNotExists();
63
+ await this.dbDriver.getTableManager().loadTables();
64
+ await new SourceRecordLoader().load();
65
+ }
66
+
67
+ async tableExists<T extends R>(table: Table<T>): Promise<boolean> {
68
+ return await this.dbDriver.getTableManager().tableExists(table);
69
+ }
70
+
71
+ async get<T extends R>(table: Table<T>, query: Query<T>): Promise<T> {
72
+ return (await this.query(table, query))[0];
73
+ }
74
+
75
+ async insert<T extends R>(table: Table<T>, record: Omit<T, keyof R>): Promise<T> {
76
+ if (!this.ignoreAuth)
77
+ this.auth.canInsert(table);
78
+
79
+ const recordCopy = Object.assign({}, record);
80
+ await this.addDefaultFieldValues(table, recordCopy);
81
+ const recordSearializer = new RecordSerializer(table);
82
+ const serializedRecord = await recordSearializer.serialize(recordCopy);
83
+ const generateInsert = (config: DbDriverStatementConfig) => new StatementFactory<T>().insert(table.name, serializedRecord as Partial<T>, this.statementConfigFactory.getStatementConfig(config));
84
+ await this.dbDriver.runDml(generateInsert);
85
+ return recordCopy as T;
86
+ }
87
+
88
+ private async addDefaultFieldValues<T extends R>(table: Table<T>, record: any) {
89
+ for (let columnPropertyName in table.columns) {
90
+ const column = (table.columns as any)[columnPropertyName] as Column<any, any>;
91
+ if (column.options?.defaultValue && typeof record[columnPropertyName] === 'undefined')
92
+ record[columnPropertyName] = await column.options.defaultValue(record);
93
+ }
94
+ }
95
+
96
+ async update<T extends R>(table: Table<T>, record: Partial<T>, query?: Query<T>): Promise<number> {
97
+ if (!this.ignoreAuth)
98
+ this.auth.canUpdate(table);
99
+
100
+ if (!query && !record.id)
101
+ throw new Error(`Update must be called with either a QueryBuilder or a record with an id property`);
102
+
103
+ const recordCopy = Object.assign({}, record);
104
+ await this.addUpdateFieldValues(table, recordCopy);
105
+ const recordSearializer = new RecordSerializer<T>(table);
106
+ const serializedRecord = await recordSearializer.serialize(recordCopy);
107
+ const qb = new QueryBuilderFactory().getQueryBuilder(table, query);
108
+ if (!query)
109
+ qb.condition({ field: 'id', operator: '=', value: recordCopy.id as T[keyof T] });
110
+
111
+ const generateUpdate = (config: DbDriverStatementConfig) => new StatementFactory<T>().update(table.name, serializedRecord as Partial<T>, qb, this.statementConfigFactory.getStatementConfig(config));
112
+ return await this.dbDriver.runDml(generateUpdate);
113
+ }
114
+
115
+ private async addUpdateFieldValues<T extends R>(table: Table<T>, record: any) {
116
+ for (let columnPropertyName in table.columns) {
117
+ const column = (table.columns as any)[columnPropertyName] as Column<any, any>;
118
+ if (column.options?.updateValue)
119
+ record[columnPropertyName] = await column.options.updateValue(record);
120
+ }
121
+ }
122
+
123
+ async delete<T extends R>(table: Table<T>, query: Query<T>): Promise<number> {
124
+ if (!this.ignoreAuth)
125
+ this.auth.canDelete(table);
126
+
127
+ const recordsToDelete = await this.query(table, query);
128
+ if (recordsToDelete.length == 0)
129
+ return 0;
130
+
131
+ const recordsToDeleteIds = recordsToDelete.map(record => record.id);
132
+ const deleteQb = new QueryBuilderFactory().getQueryBuilder(table);
133
+ deleteQb.condition({ field: 'id', operator: 'IN', value: recordsToDeleteIds as T[keyof T][] });
134
+ const generateDelete = (config: DbDriverStatementConfig) => new StatementFactory<T>().delete(table.name, deleteQb, this.statementConfigFactory.getStatementConfig(config));
135
+ await this.beforeDelete(table, recordsToDelete);
136
+ const deletedRowCount = await this.dbDriver.runDml(generateDelete);
137
+ await this.cascadeDeletions(table, recordsToDeleteIds);
138
+ return deletedRowCount;
139
+ }
140
+
141
+ private async beforeDelete(table: Table<any>, recordsToDelete: Record[]) {
142
+ for (let columnPropertyName in table.columns) {
143
+ const column = (table.columns as any)[columnPropertyName] as Column<any, any>;
144
+ if (typeof column.beforeDelete !== 'undefined')
145
+ await column.beforeDelete(table, columnPropertyName, recordsToDelete);
146
+ }
147
+ }
148
+
149
+ private async cascadeDeletions(table: Table<any>, deletedRecordIds: string[]) {
150
+ if (table.cascadeDeleteReferences().length < 1)
151
+ return;
152
+
153
+ for (let cascadeDeleteReference of table.cascadeDeleteReferences()) {
154
+ const referenceTable = tableByName(cascadeDeleteReference.table);
155
+ const referenceColumnPropertyName = getColumnPropertyName(referenceTable, cascadeDeleteReference.referenceColumn);
156
+ this.logger.info(`Executing cascade delete for table: ${table.name}, referenceTable: ${referenceTable.name}, referenceColumnPropertyName: ${referenceColumnPropertyName}, deletedRecordIds: ${JSON.stringify(deletedRecordIds)}`);
157
+ const cascadeDeleteQb = new QueryBuilderFactory().getQueryBuilder(referenceTable);
158
+ cascadeDeleteQb.condition({ field: referenceColumnPropertyName as string, operator: 'IN', value: deletedRecordIds });
159
+ const deleteCount = await this.delete(referenceTable, cascadeDeleteQb);
160
+ this.logger.info(`Cascade deleted ${deleteCount} record${deleteCount == 1 ? '' : 's'}`);
161
+ }
162
+ }
163
+
164
+ async query<T extends R>(table: Table<T>, query: Query<T>): Promise<T[]> {
165
+ if (!this.ignoreAuth)
166
+ this.auth.canQuery(table);
167
+
168
+ const qb = new QueryBuilderFactory().getQueryBuilder(table, query);
169
+ this.addColumnQueries(table, qb);
170
+ const generateQuery = (config: DbDriverStatementConfig) => qb.toSql(this.statementConfigFactory.getStatementConfig(config));
171
+ const serializedRecords = await this.dbDriver.runQuery(generateQuery);
172
+ const recordSearializer = new RecordSerializer(table);
173
+ return await Promise.all(serializedRecords.map(async (serializedRecord) => recordSearializer.deserialize(serializedRecord)));
174
+ }
175
+
176
+ async getRowCount<T extends R>(table: Table<T>, query?: Query<T>): Promise<number> {
177
+ if (!this.ignoreAuth)
178
+ this.auth.canQuery(table);
179
+
180
+ const qb = new QueryBuilderFactory().getQueryBuilder(table, query);
181
+ qb.aggregate({ function: 'COUNT', resultProp: 'count' });
182
+ this.addColumnQueries(table, qb);
183
+ const generateQuery = (config: DbDriverStatementConfig) => qb.toSql(this.statementConfigFactory.getStatementConfig(config));
184
+ const result = await this.dbDriver.runQuery(generateQuery);
185
+ return result[0]['count'];
186
+ }
187
+
188
+ private async addColumnQueries<T extends R>(table: Table<T>, qb: QueryBuilder<T>) {
189
+ for (let columnPropertyName in table.columns) {
190
+ const column = (table.columns as any)[columnPropertyName] as Column<any, any>;
191
+ if (column.options?.addToQuery)
192
+ column.options.addToQuery(qb);
193
+ }
194
+ }
195
+ }
@@ -0,0 +1,62 @@
1
+ import { Moment, moment } from './opt/moment';
2
+ import { getDb } from './Db';
3
+ import { Table } from './Table';
4
+ import { SourceRecordRepo } from './source/SourceRecordRepo';
5
+ import { MigrationRunnerService, getMigrationRunnerService } from './services/MigrationRunnerService';
6
+ import { Migration, MigrationTable } from './tables/MigrationTable';
7
+ import { Service } from '@proteinjs/service';
8
+ import { Logger } from '@proteinjs/util';
9
+
10
+ export const getMigrationRunner = () => typeof self === 'undefined' ? new MigrationRunner() : getMigrationRunnerService() as MigrationRunner;
11
+
12
+ export class MigrationRunner implements MigrationRunnerService {
13
+ private logger = new Logger(this.constructor.name);
14
+ public serviceMetadata: Service['serviceMetadata'] = {
15
+ doNotAwait: true,
16
+ };
17
+
18
+ async runMigration(id: string): Promise<void> {
19
+ const migrationTable: Table<Migration> = new MigrationTable();
20
+ const migration = new SourceRecordRepo().getSourceRecord<Migration>(migrationTable.name, id);
21
+ if (!migration)
22
+ throw new Error(`Unable to find migration source record for id: ${id}`);
23
+
24
+ const db = getDb();
25
+ migration.status = 'running';
26
+ migration.startTime = moment();
27
+ await db.update(migrationTable, migration);
28
+ this.logger.info(`Running migration (${migration.id}) ${migration.description}`);
29
+ try {
30
+ migration.output = await migration.run();
31
+ migration.status = 'success';
32
+ } catch (error: any) {
33
+ migration.failureMessage = error.message;
34
+ migration.failureStack = error.stack;
35
+ migration.status = 'failure';
36
+ } finally {
37
+ migration.endTime = moment();
38
+ }
39
+ migration.duration = this.duration(migration.startTime, migration.endTime);
40
+ await db.update(migrationTable, migration);
41
+ this.logger.info(`[${migration.status}] (${migration.duration}) Finished running migration (${migration.id}) ${migration.description}`);
42
+ }
43
+
44
+ private duration(start: Moment, end: Moment): string {
45
+ const duration = moment.duration(end.diff(start));
46
+ let parts: string[] = [];
47
+
48
+ const days = duration.days();
49
+ const hours = duration.hours();
50
+ const minutes = duration.minutes();
51
+ const seconds = duration.seconds();
52
+ const milliseconds = duration.milliseconds();
53
+
54
+ if (days > 0) parts.push(`${days} day${days > 1 ? 's' : ''}`);
55
+ if (hours > 0) parts.push(`${hours} hour${hours > 1 ? 's' : ''}`);
56
+ if (minutes > 0) parts.push(`${minutes} min${minutes > 1 ? 's' : ''}`);
57
+ if (seconds > 0) parts.push(`${seconds} sec${seconds > 1 ? 's' : ''}`);
58
+ if (days == 0 && hours == 0 && minutes == 0 && seconds == 0) parts.push(`${milliseconds} ms`);
59
+
60
+ return parts.join(' ');
61
+ }
62
+ }
@@ -0,0 +1,18 @@
1
+ import { Query } from './services/DbService';
2
+ import { Table } from './Table';
3
+ import { Record } from './Record';
4
+ import { QueryBuilder } from '@proteinjs/db-query';
5
+
6
+ export class QueryBuilderFactory {
7
+ getQueryBuilder<T extends Record>(table: Table<T>, query?: Query<T>): QueryBuilder<T> {
8
+ const qb = query ?
9
+ query instanceof QueryBuilder ?
10
+ query
11
+ :
12
+ QueryBuilder.fromObject<T>(query, table.name)
13
+ :
14
+ new QueryBuilder<T>(table.name)
15
+ ;
16
+ return qb;
17
+ }
18
+ }
package/src/Record.ts ADDED
@@ -0,0 +1,113 @@
1
+ import { DateTimeColumn, UuidColumn } from './Columns';
2
+ import { Column, Table, Columns } from './Table';
3
+ import { moment } from './opt/moment';
4
+
5
+ export interface Record {
6
+ id: string;
7
+ created: moment.Moment;
8
+ updated: moment.Moment;
9
+ }
10
+
11
+ const recordColumns: Columns<Record> = {
12
+ id: new UuidColumn('id', { ui: { hidden: true } }),
13
+ created: new DateTimeColumn('created', {
14
+ defaultValue: async () => moment(),
15
+ }),
16
+ updated: new DateTimeColumn('updated', {
17
+ defaultValue: async () => moment(),
18
+ updateValue: async () => moment(),
19
+ }),
20
+ }
21
+
22
+ /**
23
+ * Wrapper function to add default Record columns to your table's columns (should always use).
24
+ *
25
+ * Note: using this requires an explicit dependency on moment@2.29.4 in your package (since transient dependencies are brittle by typescript's standards)
26
+ *
27
+ * @param columns your columns
28
+ * @returns recordColumns & your columns
29
+ */
30
+ export function withRecordColumns<T extends Record>(columns: Columns<Omit<T, keyof Record>>): Columns<Record> & Columns<Omit<T, keyof Record>> {
31
+ return Object.assign(Object.assign({}, recordColumns), columns);
32
+ }
33
+
34
+ export type SerializedRecord = { [columnName: string]: any }
35
+
36
+ export class RecordSerializer<T extends Record> {
37
+ private table: Table<T>;
38
+
39
+ constructor(table: Table<T>) {
40
+ this.table = table;
41
+ }
42
+
43
+ async serialize(record: any): Promise<SerializedRecord> {
44
+ const serialized: any = {};
45
+ const fieldSerializer = new FieldSerializer(this.table);
46
+ for (let fieldPropertyName in record) {
47
+ if (typeof record[fieldPropertyName] === 'function')
48
+ continue;
49
+
50
+ const fieldValue = await record[fieldPropertyName];
51
+ const { columnName, serializedFieldValue } = await fieldSerializer.serialize(fieldPropertyName, fieldValue);
52
+ serialized[columnName] = serializedFieldValue;
53
+ }
54
+ return serialized;
55
+ }
56
+
57
+ async deserialize(serializedRecord: SerializedRecord): Promise<T> {
58
+ const deserialized: any = {};
59
+ const fieldSerializer = new FieldSerializer(this.table);
60
+ for (let columnName in serializedRecord) {
61
+ const serializedFieldValue = serializedRecord[columnName];
62
+ const { fieldPropertyName, fieldValue } = await fieldSerializer.deserialize(columnName, serializedFieldValue);
63
+ deserialized[fieldPropertyName] = fieldValue;
64
+ }
65
+ return deserialized;
66
+ }
67
+ }
68
+
69
+ export class FieldSerializer<T extends Record> {
70
+ constructor(
71
+ private table: Table<T>
72
+ ){}
73
+
74
+ async serialize(fieldPropertyName: string, fieldValue: any) {
75
+ const columns: {[prop: string]: Column<any, any>} = this.table.columns;
76
+ const column = columns[fieldPropertyName];
77
+ if (!column)
78
+ throw new Error(`[FieldSerializer.serialize] (${this.table.name}) no column matches fieldPropertyName: ${fieldPropertyName}`);
79
+
80
+ let serializedFieldValue = fieldValue;
81
+ if (column.serialize)
82
+ serializedFieldValue = await column.serialize(fieldValue);
83
+
84
+ return { columnName: column.name, serializedFieldValue };
85
+ }
86
+
87
+ async deserialize(columnName: string, serializedFieldValue: any) {
88
+ const columns: {[prop: string]: Column<any, any>} = this.table.columns;
89
+ let fieldPropertyName = columnName;
90
+ let column = columns[columnName]; // the scenario that the column name is the same as the property name
91
+ if (!column) {
92
+ for (let columnPropertyName in columns) {
93
+ const checkColumn = (this.table.columns as any)[columnPropertyName];
94
+ if (checkColumn && columnName == checkColumn.name) {
95
+ fieldPropertyName = columnPropertyName;
96
+ column = checkColumn;
97
+ break;
98
+ }
99
+ }
100
+ }
101
+
102
+ if (!column) {
103
+ // this is the case where a column exists in the db that is no longer defined in Table.columns
104
+ return { fieldPropertyName, fieldValue: undefined };
105
+ }
106
+
107
+ let fieldValue = serializedFieldValue
108
+ if (column.deserialize)
109
+ fieldValue = await column.deserialize(serializedFieldValue);
110
+
111
+ return { fieldPropertyName, fieldValue };
112
+ }
113
+ }
@@ -0,0 +1,44 @@
1
+ import { Db, getDb } from './Db';
2
+ import { Table } from './Table';
3
+ import { Record } from './Record';
4
+ import { Query } from './services/DbService';
5
+ import { QueryBuilderFactory } from './QueryBuilderFactory';
6
+ import { QueryBuilder } from '@proteinjs/db-query';
7
+
8
+ export class RecordIterator<T extends Record> implements AsyncIterable<T> {
9
+ private db: Db;
10
+ private table: Table<T>;
11
+ private query: QueryBuilder<T>;
12
+ private pageSize: number;
13
+ private currentPage: number;
14
+
15
+ constructor(table: Table<T>, query: Query<T>, pageSize: number = 10) {
16
+ this.db = getDb();
17
+ this.table = table;
18
+ this.query = new QueryBuilderFactory().getQueryBuilder(table, query);
19
+ this.pageSize = pageSize;
20
+ this.currentPage = 0;
21
+ }
22
+
23
+ async *[Symbol.asyncIterator](): AsyncIterator<T> {
24
+ while (true) {
25
+ const pagination = { start: this.currentPage * this.pageSize, end: (this.currentPage + 1) * this.pageSize - 1 };
26
+ this.query.paginate(pagination);
27
+ const results = await this.db.query(this.table, this.query);
28
+
29
+ if (results.length === 0) {
30
+ return;
31
+ }
32
+
33
+ for (const item of results) {
34
+ yield item;
35
+ }
36
+
37
+ if (results.length < this.pageSize) {
38
+ return;
39
+ }
40
+
41
+ this.currentPage++;
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,36 @@
1
+ import { StatementConfig } from '@proteinjs/db-query';
2
+ import { Table, tableByName } from './Table';
3
+ import { DbDriverStatementConfig } from './Db';
4
+
5
+ export class StatementConfigFactory {
6
+ private dbName: string;
7
+ private getTable: (tableName: string) => Table<any>;
8
+
9
+ constructor(
10
+ dbName: string,
11
+ getTable?: (tableName: string) => Table<any>
12
+ ) {
13
+ this.dbName = dbName;
14
+ this.getTable = getTable ? getTable : tableByName;
15
+ }
16
+
17
+ getStatementConfig(config: DbDriverStatementConfig): StatementConfig {
18
+ return {
19
+ dbName: config.prefixTablesWithDb ? this.dbName : undefined,
20
+ resolveFieldName: this.getResolveFieldName(),
21
+ useParams: config.useParams,
22
+ useNamedParams: config.useNamedParams
23
+ };
24
+ }
25
+
26
+ private getResolveFieldName() {
27
+ return (tableName: string, propertyName: string): string => {
28
+ const table = this.getTable(tableName);
29
+ const column = table.columns[propertyName];
30
+ if (!column)
31
+ throw new Error(`(${table.name}) Column does not exist for property: ${propertyName}`);
32
+
33
+ return column.name;
34
+ }
35
+ }
36
+ }
package/src/Table.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { Loadable, SourceRepository } from '@proteinjs/reflection';
2
+ import { CustomSerializableObject } from '@proteinjs/serializer';
3
+ import { Record } from './Record';
4
+ import { TableSerializerId } from './serializers/TableSerializer';
5
+ import { QueryBuilder } from '@proteinjs/db-query';
6
+ import { Identity, TableOperationsAuth } from './auth/TableAuth';
7
+
8
+ export const getTables = <T extends Record = any>() => SourceRepository.get().objects<Table<T>>('@proteinjs/db/Table');
9
+
10
+ export const tableByName = (name: string) => {
11
+ const tables = getTables();
12
+ for (let table of tables) {
13
+ if (table.name == name)
14
+ return table;
15
+ }
16
+
17
+ throw new Error(`Unable to find table: ${name}`);
18
+ }
19
+
20
+ export const getColumnPropertyName = (table: Table<any>, columnName: string) => {
21
+ for (let columnPropertyName in table.columns) {
22
+ const column = table.columns[columnPropertyName];
23
+ if (column.name == columnName)
24
+ return columnPropertyName;
25
+ }
26
+
27
+ return null;
28
+ }
29
+
30
+ export const getColumnByName = (table: Table<any>, columnName: string) => {
31
+ for (let columnPropertyName in table.columns) {
32
+ const column = table.columns[columnPropertyName];
33
+ if (column.name == columnName)
34
+ return column;
35
+ }
36
+
37
+ return null;
38
+ }
39
+
40
+ /**
41
+ * primary key is `id`
42
+ */
43
+ export abstract class Table<T extends Record> implements Loadable, CustomSerializableObject {
44
+ public __serializerId = TableSerializerId;
45
+ abstract name: string;
46
+ abstract columns: Columns<T>;
47
+ public indexes: { columns: (keyof T)[], name?: string }[] = [];
48
+ /** When records are deleted, delete records having references pointing to deleted records */
49
+ public cascadeDeleteReferences: () => { table: string, referenceColumn: string }[] = () => [];
50
+ /**
51
+ * Options for configuring SourceRecords
52
+ * @param doNotDeleteSourceRecordsFromDb if true, the SourceRecordLoader will not delete source records from the db if they no longer exist on the file system
53
+ */
54
+ public sourceRecordOptions: SourceRecordOptions = {
55
+ doNotDeleteSourceRecordsFromDb: false,
56
+ };
57
+ public auth?: {
58
+ db?: TableOperationsAuth,
59
+ service?: TableOperationsAuth,
60
+ ui?: {
61
+ recordTable?: Identity,
62
+ recordForm?: Identity,
63
+ },
64
+ };
65
+ }
66
+
67
+ type ExcludeFunctions<T> = {
68
+ [P in keyof T as T[P] extends Function ? never : P]: T[P]
69
+ }
70
+
71
+ type RequiredProps<T> = {
72
+ [P in keyof ExcludeFunctions<T>]: ExcludeFunctions<T>[P] extends undefined ? never : P
73
+ }[keyof ExcludeFunctions<T>]
74
+
75
+ type OptionalProps<T> = {
76
+ [P in keyof ExcludeFunctions<T>]: ExcludeFunctions<T>[P] extends undefined ? P : never
77
+ }[keyof ExcludeFunctions<T>]
78
+
79
+ export type Columns<T> = {
80
+ [P in RequiredProps<T>]: Column<T[P], any>
81
+ } & {
82
+ [P in OptionalProps<T>]?: Column<T[P] | undefined, any>
83
+ }
84
+
85
+ export type Column<T, Serialized> = {
86
+ name: string,
87
+ /**
88
+ * Use to rename column, will find column with `oldName` and change it to `name`.
89
+ *
90
+ * Note: after name change has happened in prod, oldName can be removed.
91
+ */
92
+ oldName?: string,
93
+ options?: ColumnOptions,
94
+ serialize?: (fieldValue: T|undefined) => Promise<Serialized|undefined>,
95
+ deserialize?: (serializedFieldValue: Serialized) => Promise<T|void>,
96
+ beforeDelete?: (table: Table<any>, columnPropertyName: string, records: any[]) => Promise<void>,
97
+ }
98
+
99
+ export type ColumnOptions = {
100
+ unique?: { unique: boolean, indexName?: string },
101
+ /**
102
+ * The column in the reference table `table` is the primary key of the table (`id` unless otherwise specified in the Table definition)
103
+ *
104
+ * Note: use a migration to drop or change an existing foreign key
105
+ */
106
+ references?: { table: string },
107
+ nullable?: boolean,
108
+ /** Value stored on insert */
109
+ defaultValue?: (insertObj: any) => Promise<any>,
110
+ /** Value stored on update */
111
+ updateValue?: (updateObj: any) => Promise<any>,
112
+ /** Add conditions to query; called on every query of this table */
113
+ addToQuery?: (qb: QueryBuilder) => Promise<void>,
114
+ ui?: {
115
+ hidden?: boolean,
116
+ },
117
+ }
118
+
119
+ export type SourceRecordOptions = {
120
+ doNotDeleteSourceRecordsFromDb?: boolean,
121
+ ui?: {
122
+ hideColumns?: boolean,
123
+ },
124
+ }
@@ -0,0 +1,60 @@
1
+ import { UserAuth } from '@proteinjs/user-auth'
2
+ import { Table } from '../Table'
3
+
4
+ /**
5
+ * @public all users, including guests (do not need to be logged in), do not need any roles
6
+ * @authenticated - users need to be logged in, do not need any roles
7
+ * @roles string[] - authenticated users, having at least one of these roles
8
+ */
9
+ export type Identity = 'public'|'authenticated'|string[]
10
+
11
+ /**
12
+ * These Identities can perform these operations on this table.
13
+ * If omitted, defaults to requiring the 'admin' role (default deny)
14
+ */
15
+ export type TableOperationsAuth = {
16
+ all?: Identity,
17
+ query?: Identity,
18
+ insert?: Identity,
19
+ update?: Identity,
20
+ delete?: Identity,
21
+ }
22
+
23
+ /**
24
+ * Util to check which table operations a user can perform
25
+ */
26
+ export class TableAuth {
27
+ private canAccess(table: Table<any>, api: 'db'|'service', operation: 'query'|'insert'|'update'|'delete'): boolean {
28
+ const tableAuth = table.auth ? table.auth[api] : undefined;
29
+ if (!tableAuth || Object.keys(tableAuth).length == 0)
30
+ return UserAuth.hasRole('admin');
31
+
32
+ return tableAuth.all === 'public'
33
+ || tableAuth[operation] === 'public'
34
+ || (tableAuth.all === 'authenticated' && UserAuth.isLoggedIn())
35
+ || (tableAuth[operation] === 'authenticated' && UserAuth.isLoggedIn())
36
+ || (Array.isArray(tableAuth.all) && UserAuth.hasRoles(tableAuth.all, 'at least one'))
37
+ || (Array.isArray(tableAuth[operation]) && UserAuth.hasRoles(tableAuth[operation] as string[], 'at least one'))
38
+ ;
39
+ }
40
+
41
+ canQuery(table: Table<any>, api: 'db'|'service' = 'db'): void {
42
+ if (!this.canAccess(table, api, 'query'))
43
+ throw new Error(`User is not authorized to query table: ${table.name}`);
44
+ }
45
+
46
+ canInsert(table: Table<any>, api: 'db'|'service' = 'db'): void {
47
+ if (!this.canAccess(table, api, 'insert'))
48
+ throw new Error(`User is not authorized to insert records into table: ${table.name}`);
49
+ }
50
+
51
+ canUpdate(table: Table<any>, api: 'db'|'service' = 'db'): void {
52
+ if (!this.canAccess(table, api, 'update'))
53
+ throw new Error(`User is not authorized to update records in table: ${table.name}`);
54
+ }
55
+
56
+ canDelete(table: Table<any>, api: 'db'|'service' = 'db'): void {
57
+ if (!this.canAccess(table, api, 'query'))
58
+ throw new Error(`User is not authorized to delete records from table: ${table.name}`);
59
+ }
60
+ }