adminforth 1.0.0

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 (58) hide show
  1. package/auth.js +66 -0
  2. package/dataConnectors/mongo.js +187 -0
  3. package/dataConnectors/postgres.js +283 -0
  4. package/dataConnectors/sqlite.js +257 -0
  5. package/index.js +722 -0
  6. package/modules/codeInjector.js +313 -0
  7. package/modules/utils.js +12 -0
  8. package/package.json +23 -0
  9. package/servers/express.js +217 -0
  10. package/spa/.eslintrc.cjs +14 -0
  11. package/spa/.vscode/extensions.json +6 -0
  12. package/spa/README.md +39 -0
  13. package/spa/env.d.ts +1 -0
  14. package/spa/index.html +23 -0
  15. package/spa/package-lock.json +4152 -0
  16. package/spa/package.json +40 -0
  17. package/spa/postcss.config.js +6 -0
  18. package/spa/public/favicon.ico +0 -0
  19. package/spa/src/App.vue +172 -0
  20. package/spa/src/assets/base.css +0 -0
  21. package/spa/src/assets/logo.svg +1 -0
  22. package/spa/src/components/AcceptModal.vue +52 -0
  23. package/spa/src/components/Breadcrumbs.vue +40 -0
  24. package/spa/src/components/BreadcrumbsWithButtons.vue +26 -0
  25. package/spa/src/components/CustomDateRangePicker.vue +218 -0
  26. package/spa/src/components/Dropdown.vue +154 -0
  27. package/spa/src/components/Filters.vue +141 -0
  28. package/spa/src/components/HelloWorld.vue +17 -0
  29. package/spa/src/components/MenuLink.vue +25 -0
  30. package/spa/src/components/ResourceForm.vue +198 -0
  31. package/spa/src/components/SingleSkeletLoader.vue +13 -0
  32. package/spa/src/components/ValueRenderer.vue +44 -0
  33. package/spa/src/components/icons/IconCalendar.vue +5 -0
  34. package/spa/src/components/icons/IconCommunity.vue +7 -0
  35. package/spa/src/components/icons/IconDocumentation.vue +7 -0
  36. package/spa/src/components/icons/IconEcosystem.vue +7 -0
  37. package/spa/src/components/icons/IconSupport.vue +7 -0
  38. package/spa/src/components/icons/IconTime.vue +5 -0
  39. package/spa/src/components/icons/IconTooling.vue +19 -0
  40. package/spa/src/index.scss +26 -0
  41. package/spa/src/main.ts +18 -0
  42. package/spa/src/router/index.ts +53 -0
  43. package/spa/src/stores/core.ts +135 -0
  44. package/spa/src/stores/modal.ts +38 -0
  45. package/spa/src/utils.ts +44 -0
  46. package/spa/src/views/CreateView.vue +103 -0
  47. package/spa/src/views/EditView.vue +95 -0
  48. package/spa/src/views/HomeView.vue +8 -0
  49. package/spa/src/views/ListView.vue +466 -0
  50. package/spa/src/views/LoginView.vue +122 -0
  51. package/spa/src/views/ResourceParent.vue +18 -0
  52. package/spa/src/views/ShowView.vue +94 -0
  53. package/spa/tailwind.config.js +12 -0
  54. package/spa/tsconfig.app.json +14 -0
  55. package/spa/tsconfig.json +11 -0
  56. package/spa/tsconfig.node.json +19 -0
  57. package/spa/vite.config.ts +42 -0
  58. package/types.js +34 -0
@@ -0,0 +1,257 @@
1
+ import betterSqlite3 from 'better-sqlite3';
2
+ import { AdminForthTypes, AdminForthFilterOperators, AdminForthSortDirections } from '../types.js';
3
+ import dayjs from 'dayjs';
4
+
5
+ class SQLiteConnector {
6
+ constructor({ url }) {
7
+ // create connection here
8
+
9
+ this.db = betterSqlite3(url.replace('sqlite://', ''));
10
+ }
11
+
12
+ async discoverFields(tableName) {
13
+ const stmt = this.db.prepare(`PRAGMA table_info(${tableName})`);
14
+ const rows = await stmt.all();
15
+ const fieldTypes = {};
16
+ rows.forEach((row) => {
17
+ const field = {};
18
+ const baseType = row.type.toLowerCase();
19
+ if (baseType == 'int') {
20
+ field.type = AdminForthTypes.INTEGER;
21
+ field._underlineType = 'int';
22
+ } else if (baseType.includes('varchar(')) {
23
+ field.type = AdminForthTypes.STRING;
24
+ field._underlineType = 'varchar';
25
+ const length = baseType.match(/\d+/);
26
+ field.maxLength = length ? parseInt(length[0]) : null;
27
+ } else if (baseType == 'text') {
28
+ field.type = AdminForthTypes.TEXT;
29
+ field._underlineType = 'text';
30
+ } else if (baseType.includes('decimal(')) {
31
+ field.type = AdminForthTypes.DECIMAL;
32
+ field._underlineType = 'decimal';
33
+ const [precision, scale] = baseType.match(/\d+/g);
34
+ field.precision = parseInt(precision);
35
+ field.scale = parseInt(scale);
36
+ } else if (baseType == 'real') {
37
+ field.type = AdminForthTypes.FLOAT; //8-byte IEEE floating point number. It
38
+ field._underlineType = 'real';
39
+ } else if (baseType == 'timestamp') {
40
+ field.type = AdminForthTypes.DATETIME;
41
+ field._underlineType = 'timestamp';
42
+ } else if (baseType == 'boolean') {
43
+ field.type = AdminForthTypes.BOOLEAN;
44
+ field._underlineType = 'boolean';
45
+ } else {
46
+ field.type = 'unknown'
47
+ }
48
+ field._baseTypeDebug = baseType;
49
+ field.required = row.notnull == 1;
50
+ field.primaryKey = row.pk == 1;
51
+ field.default = row.dflt_value;
52
+ fieldTypes[row.name] = field
53
+ });
54
+ return fieldTypes;
55
+ }
56
+
57
+ getPrimaryKey(resource) {
58
+ for (const col of resource.dataSourceColumns) {
59
+ if (col.primaryKey) {
60
+ return col.name;
61
+ }
62
+ }
63
+ }
64
+
65
+ getFieldValue(field, value) {
66
+ if (field.type == AdminForthTypes.DATETIME) {
67
+ if (!value) {
68
+ return null;
69
+ }
70
+ if (field._underlineType == 'timestamp' || field._underlineType == 'int') {
71
+ return dayjs.unix(+value).toISOString();
72
+ } else if (field._underlineType == 'varchar') {
73
+ return dayjs(value).toISOString();
74
+ } else {
75
+ throw new Error(`AdminForth does not support row type: ${field._underlineType} for timestamps, use VARCHAR (with iso strings) or TIMESTAMP/INT (with unix timestamps). Issue in field "${field.name}"`);
76
+ }
77
+ } else if (field.type == AdminForthTypes.BOOLEAN) {
78
+ return !!value;
79
+ }
80
+ return value;
81
+ }
82
+
83
+ async getRecordByPrimaryKey(resource, key) {
84
+ const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
85
+ const tableName = resource.table;
86
+ const stmt = this.db.prepare(`SELECT ${columns} FROM ${tableName} WHERE ${this.getPrimaryKey(resource)} = ?`);
87
+ const row = stmt.get(key);
88
+ if (!row) {
89
+ return null;
90
+ }
91
+ const newRow = {};
92
+ for (const [key, value] of Object.entries(row)) {
93
+ newRow[key] = this.getFieldValue(resource.dataSourceColumns.find((col) => col.name == key), value);
94
+ }
95
+ return newRow;
96
+ }
97
+
98
+ setFieldValue(field, value) {
99
+ if (field.type == AdminForthTypes.DATETIME) {
100
+ if (!value) {
101
+ return null;
102
+ }
103
+ if (field._underlineType == 'timestamp' || field._underlineType == 'int') {
104
+ // value is iso string now, convert to unix timestamp
105
+ return dayjs(value).unix();
106
+ } else if (field._underlineType == 'varchar') {
107
+ // value is iso string now, convert to unix timestamp
108
+ return dayjs(value).toISOString();
109
+ }
110
+ } else if (field.type == AdminForthTypes.BOOLEAN) {
111
+ return value ? 1 : 0;
112
+ }
113
+
114
+ return value;
115
+ }
116
+
117
+ OperatorsMap = {
118
+ [AdminForthFilterOperators.EQ]: '=',
119
+ [AdminForthFilterOperators.NE]: '!=',
120
+ [AdminForthFilterOperators.GT]: '>',
121
+ [AdminForthFilterOperators.LT]: '<',
122
+ [AdminForthFilterOperators.GTE]: '>=',
123
+ [AdminForthFilterOperators.LTE]: '<=',
124
+ [AdminForthFilterOperators.LIKE]: 'LIKE',
125
+ [AdminForthFilterOperators.ILIKE]: 'ILIKE',
126
+ [AdminForthFilterOperators.IN]: 'IN',
127
+ [AdminForthFilterOperators.NIN]: 'NOT IN',
128
+ };
129
+
130
+ SortDirectionsMap = {
131
+ [AdminForthSortDirections.ASC]: 'ASC',
132
+ [AdminForthSortDirections.DESC]: 'DESC',
133
+ };
134
+
135
+
136
+ getData({ resource, limit, offset, sort, filters }) {
137
+ const columns = resource.dataSourceColumns.map((col) => col.name).join(', ');
138
+ const tableName = resource.table;
139
+
140
+ for (const filter of filters) {
141
+ if (!this.OperatorsMap[filter.operator]) {
142
+ throw new Error(`Operator ${filter.operator} is not allowed`);
143
+ }
144
+
145
+ if (!resource.dataSourceColumns.some((col) => col.name == filter.field)) {
146
+ throw new Error(`Field "${filter.field}" is not in resource ${resource.resourceId}, available fields: ${resource.dataSourceColumns.map((col) => '"'+col.name+'"').join(', ')}`);
147
+ }
148
+ }
149
+
150
+ const where = filters.length ? `WHERE ${filters.map((f, i) => {
151
+ let placeholder = '?';
152
+ let field = f.field;
153
+ let operator = this.OperatorsMap[f.operator];
154
+ if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
155
+ placeholder = `(${f.value.map(() => '?').join(', ')})`;
156
+ } else if (f.operator == AdminForthFilterOperators.ILIKE) {
157
+ placeholder = `LOWER(?)`;
158
+ field = `LOWER(${f.field})`;
159
+ operator = 'LIKE';
160
+ }
161
+
162
+ return `${field} ${operator} ${placeholder}`
163
+ }).join(' AND ')}` : '';
164
+
165
+
166
+ const filterValues = [];
167
+
168
+ filters.length ? filters.forEach((f) => {
169
+ // for arrays do set in map
170
+ let v;
171
+ if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
172
+ v = f.value.map((val) => this.setFieldValue(resource.dataSourceColumns.find((col) => col.name == f.field), val));
173
+ } else {
174
+ v = this.setFieldValue(resource.dataSourceColumns.find((col) => col.name == f.field), f.value);
175
+ }
176
+
177
+ if (f.operator == AdminForthFilterOperators.LIKE || f.operator == AdminForthFilterOperators.ILIKE) {
178
+ filterValues.push(`%${v}%`);
179
+ } else if (f.operator == AdminForthFilterOperators.IN || f.operator == AdminForthFilterOperators.NIN) {
180
+ filterValues.push(...v);
181
+ } else {
182
+ filterValues.push(v);
183
+ }
184
+ }) : [];
185
+
186
+ const orderBy = sort.length ? `ORDER BY ${sort.map((s) => `${s.field} ${this.SortDirectionsMap[s.direction]}`).join(', ')}` : '';
187
+
188
+
189
+ const q = `SELECT ${columns} FROM ${tableName} ${where} ${orderBy} LIMIT ? OFFSET ?`;
190
+ const stmt = this.db.prepare(q);
191
+ const d = [...filterValues, limit, offset];
192
+ console.log('⚙️⚙️⚙️ running request against data', d);
193
+ const rows = stmt.all(d);
194
+
195
+ const total = this.db.prepare(`SELECT COUNT(*) FROM ${tableName} ${where}`).get([...filterValues])['COUNT(*)'];
196
+ // run all fields via getFieldValue
197
+ return {
198
+ data: rows.map((row) => {
199
+ const newRow = {};
200
+ for (const [key, value] of Object.entries(row)) {
201
+ newRow[key] = this.getFieldValue(resource.dataSourceColumns.find((col) => col.name == key), value);
202
+ }
203
+ return newRow;
204
+ }),
205
+ total,
206
+ };
207
+ }
208
+
209
+ async getMinMaxForColumns({ resource, columns }) {
210
+ const tableName = resource.table;
211
+ const result = {};
212
+ await Promise.all(columns.map(async (col) => {
213
+ const stmt = await this.db.prepare(`SELECT MIN(${col.name}) as min, MAX(${col.name}) as max FROM ${tableName}`);
214
+ const { min, max } = stmt.get();
215
+ result[col.name] = {
216
+ min: this.getFieldValue(col, min),
217
+ max: this.getFieldValue(col, max),
218
+ };
219
+ }))
220
+ return result;
221
+ }
222
+
223
+ async createRecord({ resource, record }) {
224
+ const tableName = resource.table;
225
+ const columns = Object.keys(record);
226
+ const placeholders = columns.map(() => '?').join(', ');
227
+ const values = columns.map((colName) => {
228
+ const col = resource.dataSourceColumns.find((col) => col.name == colName);
229
+ if (col) {
230
+ return this.setFieldValue(col, record[colName])
231
+ } else {
232
+ return record[colName];
233
+ }
234
+ });
235
+ this.db.prepare(`INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`).run(values);
236
+ }
237
+
238
+ async updateRecord({ resource, recordId, record, newValues }) {
239
+ const columns = Object.keys(newValues).map((col) => col).join(', ');
240
+ const placeholders = Object.keys(newValues).map(() => '?').join(', ');
241
+ const values = [...Object.values(newValues), recordId];
242
+
243
+ this.db.prepare(
244
+ `UPDATE ${resource.table} SET ${columns} = ${placeholders} WHERE ${this.getPrimaryKey(resource)} = ?`
245
+ ).run(values);
246
+ }
247
+
248
+ async deleteRecord({ resource, recordId }) {
249
+ this.db.prepare(`DELETE FROM ${resource.table} WHERE ${this.getPrimaryKey(resource)} = ?`).run(recordId);
250
+ }
251
+
252
+ close() {
253
+ this.db.close();
254
+ }
255
+ }
256
+
257
+ export default SQLiteConnector;