@openmdm/core 0.2.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.
package/src/schema.ts ADDED
@@ -0,0 +1,533 @@
1
+ /**
2
+ * OpenMDM Database Schema Definition
3
+ *
4
+ * This schema defines the structure for MDM data storage.
5
+ * Database adapters implement this schema for their specific ORM/database.
6
+ *
7
+ * Tables:
8
+ * - mdm_devices: Enrolled devices and their state
9
+ * - mdm_policies: Device policies and configurations
10
+ * - mdm_applications: Registered applications for deployment
11
+ * - mdm_commands: Command queue for device operations
12
+ * - mdm_events: Event log for device activities
13
+ * - mdm_groups: Device grouping for bulk operations
14
+ * - mdm_device_groups: Many-to-many device-group relationships
15
+ * - mdm_push_tokens: FCM/MQTT push notification tokens
16
+ * - mdm_app_deployments: App-to-policy/group deployment mappings
17
+ * - mdm_app_versions: App version history for rollback support
18
+ * - mdm_rollbacks: Rollback operation history and status
19
+ * - mdm_webhook_endpoints: Outbound webhook configuration
20
+ * - mdm_webhook_deliveries: Webhook delivery history
21
+ */
22
+
23
+ // ============================================
24
+ // Schema Column Types
25
+ // ============================================
26
+
27
+ export type ColumnType =
28
+ | 'string'
29
+ | 'text'
30
+ | 'integer'
31
+ | 'bigint'
32
+ | 'boolean'
33
+ | 'datetime'
34
+ | 'json'
35
+ | 'enum';
36
+
37
+ export interface ColumnDefinition {
38
+ type: ColumnType;
39
+ nullable?: boolean;
40
+ primaryKey?: boolean;
41
+ unique?: boolean;
42
+ default?: unknown;
43
+ enumValues?: string[];
44
+ references?: {
45
+ table: string;
46
+ column: string;
47
+ onDelete?: 'cascade' | 'set null' | 'restrict';
48
+ };
49
+ }
50
+
51
+ export interface IndexDefinition {
52
+ columns: string[];
53
+ unique?: boolean;
54
+ name?: string;
55
+ }
56
+
57
+ export interface TableDefinition {
58
+ columns: Record<string, ColumnDefinition>;
59
+ indexes?: IndexDefinition[];
60
+ }
61
+
62
+ export interface SchemaDefinition {
63
+ tables: Record<string, TableDefinition>;
64
+ }
65
+
66
+ // ============================================
67
+ // OpenMDM Schema
68
+ // ============================================
69
+
70
+ export const mdmSchema: SchemaDefinition = {
71
+ tables: {
72
+ // ----------------------------------------
73
+ // Devices Table
74
+ // ----------------------------------------
75
+ mdm_devices: {
76
+ columns: {
77
+ id: { type: 'string', primaryKey: true },
78
+ external_id: { type: 'string', nullable: true },
79
+ enrollment_id: { type: 'string', unique: true },
80
+ status: {
81
+ type: 'enum',
82
+ enumValues: ['pending', 'enrolled', 'unenrolled', 'blocked'],
83
+ default: 'pending',
84
+ },
85
+
86
+ // Device Info
87
+ model: { type: 'string', nullable: true },
88
+ manufacturer: { type: 'string', nullable: true },
89
+ os_version: { type: 'string', nullable: true },
90
+ serial_number: { type: 'string', nullable: true },
91
+ imei: { type: 'string', nullable: true },
92
+ mac_address: { type: 'string', nullable: true },
93
+ android_id: { type: 'string', nullable: true },
94
+
95
+ // MDM State
96
+ policy_id: {
97
+ type: 'string',
98
+ nullable: true,
99
+ references: { table: 'mdm_policies', column: 'id', onDelete: 'set null' },
100
+ },
101
+ last_heartbeat: { type: 'datetime', nullable: true },
102
+ last_sync: { type: 'datetime', nullable: true },
103
+
104
+ // Telemetry (denormalized for quick access)
105
+ battery_level: { type: 'integer', nullable: true },
106
+ storage_used: { type: 'bigint', nullable: true },
107
+ storage_total: { type: 'bigint', nullable: true },
108
+ latitude: { type: 'string', nullable: true }, // Stored as string for precision
109
+ longitude: { type: 'string', nullable: true },
110
+ location_timestamp: { type: 'datetime', nullable: true },
111
+
112
+ // JSON fields
113
+ installed_apps: { type: 'json', nullable: true },
114
+ tags: { type: 'json', nullable: true },
115
+ metadata: { type: 'json', nullable: true },
116
+
117
+ // Timestamps
118
+ created_at: { type: 'datetime', default: 'now' },
119
+ updated_at: { type: 'datetime', default: 'now' },
120
+ },
121
+ indexes: [
122
+ { columns: ['enrollment_id'], unique: true },
123
+ { columns: ['status'] },
124
+ { columns: ['policy_id'] },
125
+ { columns: ['last_heartbeat'] },
126
+ { columns: ['mac_address'] },
127
+ { columns: ['serial_number'] },
128
+ ],
129
+ },
130
+
131
+ // ----------------------------------------
132
+ // Policies Table
133
+ // ----------------------------------------
134
+ mdm_policies: {
135
+ columns: {
136
+ id: { type: 'string', primaryKey: true },
137
+ name: { type: 'string' },
138
+ description: { type: 'text', nullable: true },
139
+ is_default: { type: 'boolean', default: false },
140
+ settings: { type: 'json' },
141
+ created_at: { type: 'datetime', default: 'now' },
142
+ updated_at: { type: 'datetime', default: 'now' },
143
+ },
144
+ indexes: [
145
+ { columns: ['name'] },
146
+ { columns: ['is_default'] },
147
+ ],
148
+ },
149
+
150
+ // ----------------------------------------
151
+ // Applications Table
152
+ // ----------------------------------------
153
+ mdm_applications: {
154
+ columns: {
155
+ id: { type: 'string', primaryKey: true },
156
+ name: { type: 'string' },
157
+ package_name: { type: 'string' },
158
+ version: { type: 'string' },
159
+ version_code: { type: 'integer' },
160
+ url: { type: 'string' },
161
+ hash: { type: 'string', nullable: true }, // SHA-256
162
+ size: { type: 'bigint', nullable: true },
163
+ min_sdk_version: { type: 'integer', nullable: true },
164
+
165
+ // Deployment settings
166
+ show_icon: { type: 'boolean', default: true },
167
+ run_after_install: { type: 'boolean', default: false },
168
+ run_at_boot: { type: 'boolean', default: false },
169
+ is_system: { type: 'boolean', default: false },
170
+
171
+ // State
172
+ is_active: { type: 'boolean', default: true },
173
+
174
+ // Metadata
175
+ metadata: { type: 'json', nullable: true },
176
+ created_at: { type: 'datetime', default: 'now' },
177
+ updated_at: { type: 'datetime', default: 'now' },
178
+ },
179
+ indexes: [
180
+ { columns: ['package_name'] },
181
+ { columns: ['package_name', 'version'], unique: true },
182
+ { columns: ['is_active'] },
183
+ ],
184
+ },
185
+
186
+ // ----------------------------------------
187
+ // Commands Table
188
+ // ----------------------------------------
189
+ mdm_commands: {
190
+ columns: {
191
+ id: { type: 'string', primaryKey: true },
192
+ device_id: {
193
+ type: 'string',
194
+ references: { table: 'mdm_devices', column: 'id', onDelete: 'cascade' },
195
+ },
196
+ type: { type: 'string' },
197
+ payload: { type: 'json', nullable: true },
198
+ status: {
199
+ type: 'enum',
200
+ enumValues: ['pending', 'sent', 'acknowledged', 'completed', 'failed', 'cancelled'],
201
+ default: 'pending',
202
+ },
203
+ result: { type: 'json', nullable: true },
204
+ error: { type: 'text', nullable: true },
205
+ created_at: { type: 'datetime', default: 'now' },
206
+ sent_at: { type: 'datetime', nullable: true },
207
+ acknowledged_at: { type: 'datetime', nullable: true },
208
+ completed_at: { type: 'datetime', nullable: true },
209
+ },
210
+ indexes: [
211
+ { columns: ['device_id'] },
212
+ { columns: ['status'] },
213
+ { columns: ['device_id', 'status'] },
214
+ { columns: ['created_at'] },
215
+ ],
216
+ },
217
+
218
+ // ----------------------------------------
219
+ // Events Table
220
+ // ----------------------------------------
221
+ mdm_events: {
222
+ columns: {
223
+ id: { type: 'string', primaryKey: true },
224
+ device_id: {
225
+ type: 'string',
226
+ references: { table: 'mdm_devices', column: 'id', onDelete: 'cascade' },
227
+ },
228
+ type: { type: 'string' },
229
+ payload: { type: 'json' },
230
+ created_at: { type: 'datetime', default: 'now' },
231
+ },
232
+ indexes: [
233
+ { columns: ['device_id'] },
234
+ { columns: ['type'] },
235
+ { columns: ['device_id', 'type'] },
236
+ { columns: ['created_at'] },
237
+ ],
238
+ },
239
+
240
+ // ----------------------------------------
241
+ // Groups Table
242
+ // ----------------------------------------
243
+ mdm_groups: {
244
+ columns: {
245
+ id: { type: 'string', primaryKey: true },
246
+ name: { type: 'string' },
247
+ description: { type: 'text', nullable: true },
248
+ policy_id: {
249
+ type: 'string',
250
+ nullable: true,
251
+ references: { table: 'mdm_policies', column: 'id', onDelete: 'set null' },
252
+ },
253
+ parent_id: {
254
+ type: 'string',
255
+ nullable: true,
256
+ references: { table: 'mdm_groups', column: 'id', onDelete: 'set null' },
257
+ },
258
+ metadata: { type: 'json', nullable: true },
259
+ created_at: { type: 'datetime', default: 'now' },
260
+ updated_at: { type: 'datetime', default: 'now' },
261
+ },
262
+ indexes: [
263
+ { columns: ['name'] },
264
+ { columns: ['policy_id'] },
265
+ { columns: ['parent_id'] },
266
+ ],
267
+ },
268
+
269
+ // ----------------------------------------
270
+ // Device Groups (Many-to-Many)
271
+ // ----------------------------------------
272
+ mdm_device_groups: {
273
+ columns: {
274
+ device_id: {
275
+ type: 'string',
276
+ references: { table: 'mdm_devices', column: 'id', onDelete: 'cascade' },
277
+ },
278
+ group_id: {
279
+ type: 'string',
280
+ references: { table: 'mdm_groups', column: 'id', onDelete: 'cascade' },
281
+ },
282
+ created_at: { type: 'datetime', default: 'now' },
283
+ },
284
+ indexes: [
285
+ { columns: ['device_id', 'group_id'], unique: true },
286
+ { columns: ['group_id'] },
287
+ ],
288
+ },
289
+
290
+ // ----------------------------------------
291
+ // Push Tokens (for FCM/MQTT registration)
292
+ // ----------------------------------------
293
+ mdm_push_tokens: {
294
+ columns: {
295
+ id: { type: 'string', primaryKey: true },
296
+ device_id: {
297
+ type: 'string',
298
+ references: { table: 'mdm_devices', column: 'id', onDelete: 'cascade' },
299
+ },
300
+ provider: {
301
+ type: 'enum',
302
+ enumValues: ['fcm', 'mqtt', 'websocket'],
303
+ },
304
+ token: { type: 'string' },
305
+ is_active: { type: 'boolean', default: true },
306
+ created_at: { type: 'datetime', default: 'now' },
307
+ updated_at: { type: 'datetime', default: 'now' },
308
+ },
309
+ indexes: [
310
+ { columns: ['device_id'] },
311
+ { columns: ['provider', 'token'], unique: true },
312
+ { columns: ['is_active'] },
313
+ ],
314
+ },
315
+
316
+ // ----------------------------------------
317
+ // Application Deployments (Which apps go to which policies/groups)
318
+ // ----------------------------------------
319
+ mdm_app_deployments: {
320
+ columns: {
321
+ id: { type: 'string', primaryKey: true },
322
+ application_id: {
323
+ type: 'string',
324
+ references: { table: 'mdm_applications', column: 'id', onDelete: 'cascade' },
325
+ },
326
+ // Target can be policy or group
327
+ target_type: {
328
+ type: 'enum',
329
+ enumValues: ['policy', 'group'],
330
+ },
331
+ target_id: { type: 'string' },
332
+ action: {
333
+ type: 'enum',
334
+ enumValues: ['install', 'update', 'uninstall'],
335
+ default: 'install',
336
+ },
337
+ is_required: { type: 'boolean', default: false },
338
+ created_at: { type: 'datetime', default: 'now' },
339
+ },
340
+ indexes: [
341
+ { columns: ['application_id'] },
342
+ { columns: ['target_type', 'target_id'] },
343
+ ],
344
+ },
345
+
346
+ // ----------------------------------------
347
+ // App Versions (Version history for rollback support)
348
+ // ----------------------------------------
349
+ mdm_app_versions: {
350
+ columns: {
351
+ id: { type: 'string', primaryKey: true },
352
+ application_id: {
353
+ type: 'string',
354
+ references: { table: 'mdm_applications', column: 'id', onDelete: 'cascade' },
355
+ },
356
+ package_name: { type: 'string' },
357
+ version: { type: 'string' },
358
+ version_code: { type: 'integer' },
359
+ url: { type: 'string' },
360
+ hash: { type: 'string', nullable: true },
361
+ size: { type: 'bigint', nullable: true },
362
+ release_notes: { type: 'text', nullable: true },
363
+ is_minimum_version: { type: 'boolean', default: false },
364
+ created_at: { type: 'datetime', default: 'now' },
365
+ },
366
+ indexes: [
367
+ { columns: ['application_id'] },
368
+ { columns: ['package_name'] },
369
+ { columns: ['package_name', 'version_code'], unique: true },
370
+ { columns: ['is_minimum_version'] },
371
+ ],
372
+ },
373
+
374
+ // ----------------------------------------
375
+ // App Rollbacks (Rollback history and status)
376
+ // ----------------------------------------
377
+ mdm_rollbacks: {
378
+ columns: {
379
+ id: { type: 'string', primaryKey: true },
380
+ device_id: {
381
+ type: 'string',
382
+ references: { table: 'mdm_devices', column: 'id', onDelete: 'cascade' },
383
+ },
384
+ package_name: { type: 'string' },
385
+ from_version: { type: 'string' },
386
+ from_version_code: { type: 'integer' },
387
+ to_version: { type: 'string' },
388
+ to_version_code: { type: 'integer' },
389
+ reason: { type: 'text', nullable: true },
390
+ status: {
391
+ type: 'enum',
392
+ enumValues: ['pending', 'in_progress', 'completed', 'failed'],
393
+ default: 'pending',
394
+ },
395
+ error: { type: 'text', nullable: true },
396
+ initiated_by: { type: 'string', nullable: true },
397
+ created_at: { type: 'datetime', default: 'now' },
398
+ completed_at: { type: 'datetime', nullable: true },
399
+ },
400
+ indexes: [
401
+ { columns: ['device_id'] },
402
+ { columns: ['package_name'] },
403
+ { columns: ['device_id', 'package_name'] },
404
+ { columns: ['status'] },
405
+ { columns: ['created_at'] },
406
+ ],
407
+ },
408
+
409
+ // ----------------------------------------
410
+ // Webhook Endpoints (For outbound webhook configuration storage)
411
+ // ----------------------------------------
412
+ mdm_webhook_endpoints: {
413
+ columns: {
414
+ id: { type: 'string', primaryKey: true },
415
+ url: { type: 'string' },
416
+ events: { type: 'json' }, // Array of event types or ['*']
417
+ headers: { type: 'json', nullable: true },
418
+ enabled: { type: 'boolean', default: true },
419
+ description: { type: 'text', nullable: true },
420
+ created_at: { type: 'datetime', default: 'now' },
421
+ updated_at: { type: 'datetime', default: 'now' },
422
+ },
423
+ indexes: [
424
+ { columns: ['enabled'] },
425
+ ],
426
+ },
427
+
428
+ // ----------------------------------------
429
+ // Webhook Deliveries (Delivery history and status)
430
+ // ----------------------------------------
431
+ mdm_webhook_deliveries: {
432
+ columns: {
433
+ id: { type: 'string', primaryKey: true },
434
+ endpoint_id: {
435
+ type: 'string',
436
+ references: { table: 'mdm_webhook_endpoints', column: 'id', onDelete: 'cascade' },
437
+ },
438
+ event_id: { type: 'string' },
439
+ event_type: { type: 'string' },
440
+ payload: { type: 'json' },
441
+ status: {
442
+ type: 'enum',
443
+ enumValues: ['pending', 'success', 'failed'],
444
+ default: 'pending',
445
+ },
446
+ status_code: { type: 'integer', nullable: true },
447
+ error: { type: 'text', nullable: true },
448
+ retry_count: { type: 'integer', default: 0 },
449
+ created_at: { type: 'datetime', default: 'now' },
450
+ delivered_at: { type: 'datetime', nullable: true },
451
+ },
452
+ indexes: [
453
+ { columns: ['endpoint_id'] },
454
+ { columns: ['event_type'] },
455
+ { columns: ['status'] },
456
+ { columns: ['created_at'] },
457
+ ],
458
+ },
459
+ },
460
+ };
461
+
462
+ // ============================================
463
+ // Schema Helper Functions
464
+ // ============================================
465
+
466
+ /**
467
+ * Get all table names from the schema
468
+ */
469
+ export function getTableNames(): string[] {
470
+ return Object.keys(mdmSchema.tables);
471
+ }
472
+
473
+ /**
474
+ * Get column names for a table
475
+ */
476
+ export function getColumnNames(tableName: string): string[] {
477
+ const table = mdmSchema.tables[tableName];
478
+ if (!table) throw new Error(`Table ${tableName} not found in schema`);
479
+ return Object.keys(table.columns);
480
+ }
481
+
482
+ /**
483
+ * Get the primary key column for a table
484
+ */
485
+ export function getPrimaryKey(tableName: string): string | null {
486
+ const table = mdmSchema.tables[tableName];
487
+ if (!table) throw new Error(`Table ${tableName} not found in schema`);
488
+
489
+ for (const [name, def] of Object.entries(table.columns)) {
490
+ if (def.primaryKey) return name;
491
+ }
492
+ return null;
493
+ }
494
+
495
+ /**
496
+ * Convert snake_case column name to camelCase
497
+ */
498
+ export function snakeToCamel(str: string): string {
499
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
500
+ }
501
+
502
+ /**
503
+ * Convert camelCase to snake_case
504
+ */
505
+ export function camelToSnake(str: string): string {
506
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
507
+ }
508
+
509
+ /**
510
+ * Transform object keys from snake_case to camelCase
511
+ */
512
+ export function transformToCamelCase<T extends Record<string, unknown>>(
513
+ obj: T
514
+ ): Record<string, unknown> {
515
+ const result: Record<string, unknown> = {};
516
+ for (const [key, value] of Object.entries(obj)) {
517
+ result[snakeToCamel(key)] = value;
518
+ }
519
+ return result;
520
+ }
521
+
522
+ /**
523
+ * Transform object keys from camelCase to snake_case
524
+ */
525
+ export function transformToSnakeCase<T extends Record<string, unknown>>(
526
+ obj: T
527
+ ): Record<string, unknown> {
528
+ const result: Record<string, unknown> = {};
529
+ for (const [key, value] of Object.entries(obj)) {
530
+ result[camelToSnake(key)] = value;
531
+ }
532
+ return result;
533
+ }