@openmdm/drizzle-adapter 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/index.ts ADDED
@@ -0,0 +1,914 @@
1
+ /**
2
+ * OpenMDM Drizzle Adapter
3
+ *
4
+ * Database adapter for Drizzle ORM, supporting PostgreSQL, MySQL, and SQLite.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { drizzle } from 'drizzle-orm/node-postgres';
9
+ * import { drizzleAdapter } from '@openmdm/drizzle-adapter';
10
+ * import { mdmSchema } from '@openmdm/drizzle-adapter/postgres';
11
+ * import { createMDM } from '@openmdm/core';
12
+ *
13
+ * const pool = new Pool({ connectionString: DATABASE_URL });
14
+ * const db = drizzle(pool, { schema: mdmSchema });
15
+ *
16
+ * const mdm = createMDM({
17
+ * database: drizzleAdapter(db),
18
+ * // ...
19
+ * });
20
+ * ```
21
+ */
22
+
23
+ import { eq, and, or, like, inArray, desc, sql, isNull, type SQL } from 'drizzle-orm';
24
+ import { nanoid } from 'nanoid';
25
+ import type {
26
+ DatabaseAdapter,
27
+ Device,
28
+ DeviceFilter,
29
+ DeviceListResult,
30
+ CreateDeviceInput,
31
+ UpdateDeviceInput,
32
+ Policy,
33
+ CreatePolicyInput,
34
+ UpdatePolicyInput,
35
+ Application,
36
+ CreateApplicationInput,
37
+ UpdateApplicationInput,
38
+ Command,
39
+ SendCommandInput,
40
+ CommandFilter,
41
+ MDMEvent,
42
+ EventFilter,
43
+ Group,
44
+ CreateGroupInput,
45
+ UpdateGroupInput,
46
+ PushToken,
47
+ RegisterPushTokenInput,
48
+ PolicySettings,
49
+ InstalledApp,
50
+ DeviceLocation,
51
+ } from '@openmdm/core';
52
+
53
+ // Import postgres schema types
54
+ import type {
55
+ mdmDevices,
56
+ mdmPolicies,
57
+ mdmApplications,
58
+ mdmCommands,
59
+ mdmEvents,
60
+ mdmGroups,
61
+ mdmDeviceGroups,
62
+ mdmPushTokens,
63
+ } from './postgres';
64
+
65
+ // Type for Drizzle database instance
66
+ type DrizzleDB = {
67
+ select: (columns?: unknown) => unknown;
68
+ insert: (table: unknown) => unknown;
69
+ update: (table: unknown) => unknown;
70
+ delete: (table: unknown) => unknown;
71
+ query: Record<string, unknown>;
72
+ transaction: <T>(fn: (tx: DrizzleDB) => Promise<T>) => Promise<T>;
73
+ };
74
+
75
+ export interface DrizzleAdapterOptions {
76
+ /**
77
+ * Table references - pass your imported Drizzle tables
78
+ */
79
+ tables: {
80
+ devices: typeof mdmDevices;
81
+ policies: typeof mdmPolicies;
82
+ applications: typeof mdmApplications;
83
+ commands: typeof mdmCommands;
84
+ events: typeof mdmEvents;
85
+ groups: typeof mdmGroups;
86
+ deviceGroups: typeof mdmDeviceGroups;
87
+ pushTokens: typeof mdmPushTokens;
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Create a Drizzle database adapter for OpenMDM
93
+ */
94
+ export function drizzleAdapter(
95
+ db: DrizzleDB,
96
+ options: DrizzleAdapterOptions
97
+ ): DatabaseAdapter {
98
+ const { tables } = options;
99
+ const {
100
+ devices,
101
+ policies,
102
+ applications,
103
+ commands,
104
+ events,
105
+ groups,
106
+ deviceGroups,
107
+ pushTokens,
108
+ } = tables;
109
+
110
+ // Helper to generate IDs
111
+ const generateId = () => nanoid(21);
112
+
113
+ // Helper to transform DB row to Device
114
+ const toDevice = (row: Record<string, unknown>): Device => ({
115
+ id: row.id as string,
116
+ externalId: row.externalId as string | null,
117
+ enrollmentId: row.enrollmentId as string,
118
+ status: row.status as Device['status'],
119
+ model: row.model as string | null,
120
+ manufacturer: row.manufacturer as string | null,
121
+ osVersion: row.osVersion as string | null,
122
+ serialNumber: row.serialNumber as string | null,
123
+ imei: row.imei as string | null,
124
+ macAddress: row.macAddress as string | null,
125
+ androidId: row.androidId as string | null,
126
+ policyId: row.policyId as string | null,
127
+ lastHeartbeat: row.lastHeartbeat as Date | null,
128
+ lastSync: row.lastSync as Date | null,
129
+ batteryLevel: row.batteryLevel as number | null,
130
+ storageUsed: row.storageUsed as number | null,
131
+ storageTotal: row.storageTotal as number | null,
132
+ location:
133
+ row.latitude && row.longitude
134
+ ? {
135
+ latitude: parseFloat(row.latitude as string),
136
+ longitude: parseFloat(row.longitude as string),
137
+ timestamp: row.locationTimestamp as Date,
138
+ }
139
+ : null,
140
+ installedApps: row.installedApps as InstalledApp[] | null,
141
+ tags: row.tags as Record<string, string> | null,
142
+ metadata: row.metadata as Record<string, unknown> | null,
143
+ createdAt: row.createdAt as Date,
144
+ updatedAt: row.updatedAt as Date,
145
+ });
146
+
147
+ // Helper to transform DB row to Policy
148
+ const toPolicy = (row: Record<string, unknown>): Policy => ({
149
+ id: row.id as string,
150
+ name: row.name as string,
151
+ description: row.description as string | null,
152
+ isDefault: row.isDefault as boolean,
153
+ settings: row.settings as PolicySettings,
154
+ createdAt: row.createdAt as Date,
155
+ updatedAt: row.updatedAt as Date,
156
+ });
157
+
158
+ // Helper to transform DB row to Application
159
+ const toApplication = (row: Record<string, unknown>): Application => ({
160
+ id: row.id as string,
161
+ name: row.name as string,
162
+ packageName: row.packageName as string,
163
+ version: row.version as string,
164
+ versionCode: row.versionCode as number,
165
+ url: row.url as string,
166
+ hash: row.hash as string | null,
167
+ size: row.size as number | null,
168
+ minSdkVersion: row.minSdkVersion as number | null,
169
+ showIcon: row.showIcon as boolean,
170
+ runAfterInstall: row.runAfterInstall as boolean,
171
+ runAtBoot: row.runAtBoot as boolean,
172
+ isSystem: row.isSystem as boolean,
173
+ isActive: row.isActive as boolean,
174
+ metadata: row.metadata as Record<string, unknown> | null,
175
+ createdAt: row.createdAt as Date,
176
+ updatedAt: row.updatedAt as Date,
177
+ });
178
+
179
+ // Helper to transform DB row to Command
180
+ const toCommand = (row: Record<string, unknown>): Command => ({
181
+ id: row.id as string,
182
+ deviceId: row.deviceId as string,
183
+ type: row.type as Command['type'],
184
+ payload: row.payload as Record<string, unknown> | null,
185
+ status: row.status as Command['status'],
186
+ result: row.result as Command['result'],
187
+ error: row.error as string | null,
188
+ createdAt: row.createdAt as Date,
189
+ sentAt: row.sentAt as Date | null,
190
+ acknowledgedAt: row.acknowledgedAt as Date | null,
191
+ completedAt: row.completedAt as Date | null,
192
+ });
193
+
194
+ // Helper to transform DB row to Event
195
+ const toEvent = (row: Record<string, unknown>): MDMEvent => ({
196
+ id: row.id as string,
197
+ deviceId: row.deviceId as string,
198
+ type: row.type as MDMEvent['type'],
199
+ payload: row.payload as Record<string, unknown>,
200
+ createdAt: row.createdAt as Date,
201
+ });
202
+
203
+ // Helper to transform DB row to Group
204
+ const toGroup = (row: Record<string, unknown>): Group => ({
205
+ id: row.id as string,
206
+ name: row.name as string,
207
+ description: row.description as string | null,
208
+ policyId: row.policyId as string | null,
209
+ parentId: row.parentId as string | null,
210
+ metadata: row.metadata as Record<string, unknown> | null,
211
+ createdAt: row.createdAt as Date,
212
+ updatedAt: row.updatedAt as Date,
213
+ });
214
+
215
+ // Helper to transform DB row to PushToken
216
+ const toPushToken = (row: Record<string, unknown>): PushToken => ({
217
+ id: row.id as string,
218
+ deviceId: row.deviceId as string,
219
+ provider: row.provider as PushToken['provider'],
220
+ token: row.token as string,
221
+ isActive: row.isActive as boolean,
222
+ createdAt: row.createdAt as Date,
223
+ updatedAt: row.updatedAt as Date,
224
+ });
225
+
226
+ return {
227
+ // ============================================
228
+ // Device Methods
229
+ // ============================================
230
+
231
+ async findDevice(id: string): Promise<Device | null> {
232
+ const result = await (db as any)
233
+ .select()
234
+ .from(devices)
235
+ .where(eq(devices.id, id))
236
+ .limit(1);
237
+ return result[0] ? toDevice(result[0]) : null;
238
+ },
239
+
240
+ async findDeviceByEnrollmentId(
241
+ enrollmentId: string
242
+ ): Promise<Device | null> {
243
+ const result = await (db as any)
244
+ .select()
245
+ .from(devices)
246
+ .where(eq(devices.enrollmentId, enrollmentId))
247
+ .limit(1);
248
+ return result[0] ? toDevice(result[0]) : null;
249
+ },
250
+
251
+ async listDevices(filter?: DeviceFilter): Promise<DeviceListResult> {
252
+ const limit = filter?.limit ?? 100;
253
+ const offset = filter?.offset ?? 0;
254
+
255
+ let query = (db as any).select().from(devices);
256
+
257
+ // Build WHERE conditions
258
+ const conditions: (SQL | undefined)[] = [];
259
+
260
+ if (filter?.status) {
261
+ if (Array.isArray(filter.status)) {
262
+ conditions.push(inArray(devices.status, filter.status));
263
+ } else {
264
+ conditions.push(eq(devices.status, filter.status));
265
+ }
266
+ }
267
+
268
+ if (filter?.policyId) {
269
+ conditions.push(eq(devices.policyId, filter.policyId));
270
+ }
271
+
272
+ if (filter?.search) {
273
+ const searchPattern = `%${filter.search}%`;
274
+ conditions.push(
275
+ or(
276
+ like(devices.model, searchPattern),
277
+ like(devices.manufacturer, searchPattern),
278
+ like(devices.enrollmentId, searchPattern),
279
+ like(devices.serialNumber, searchPattern)
280
+ )
281
+ );
282
+ }
283
+
284
+ if (conditions.length > 0) {
285
+ query = query.where(and(...conditions));
286
+ }
287
+
288
+ // Get total count
289
+ const countResult = await (db as any)
290
+ .select({ count: sql<number>`count(*)` })
291
+ .from(devices)
292
+ .where(conditions.length > 0 ? and(...conditions) : undefined);
293
+ const total = Number(countResult[0]?.count ?? 0);
294
+
295
+ // Get paginated results
296
+ const result = await query
297
+ .orderBy(desc(devices.createdAt))
298
+ .limit(limit)
299
+ .offset(offset);
300
+
301
+ return {
302
+ devices: result.map(toDevice),
303
+ total,
304
+ limit,
305
+ offset,
306
+ };
307
+ },
308
+
309
+ async createDevice(data: CreateDeviceInput): Promise<Device> {
310
+ const id = generateId();
311
+ const now = new Date();
312
+
313
+ const deviceData = {
314
+ id,
315
+ enrollmentId: data.enrollmentId,
316
+ externalId: data.externalId ?? null,
317
+ status: 'pending' as const,
318
+ model: data.model ?? null,
319
+ manufacturer: data.manufacturer ?? null,
320
+ osVersion: data.osVersion ?? null,
321
+ serialNumber: data.serialNumber ?? null,
322
+ imei: data.imei ?? null,
323
+ macAddress: data.macAddress ?? null,
324
+ androidId: data.androidId ?? null,
325
+ policyId: data.policyId ?? null,
326
+ tags: data.tags ?? null,
327
+ metadata: data.metadata ?? null,
328
+ createdAt: now,
329
+ updatedAt: now,
330
+ };
331
+
332
+ await (db as any).insert(devices).values(deviceData);
333
+
334
+ return this.findDevice(id) as Promise<Device>;
335
+ },
336
+
337
+ async updateDevice(id: string, data: UpdateDeviceInput): Promise<Device> {
338
+ const updateData: Record<string, unknown> = {
339
+ updatedAt: new Date(),
340
+ };
341
+
342
+ if (data.externalId !== undefined) updateData.externalId = data.externalId;
343
+ if (data.status !== undefined) updateData.status = data.status;
344
+ if (data.policyId !== undefined) updateData.policyId = data.policyId;
345
+ if (data.model !== undefined) updateData.model = data.model;
346
+ if (data.manufacturer !== undefined)
347
+ updateData.manufacturer = data.manufacturer;
348
+ if (data.osVersion !== undefined) updateData.osVersion = data.osVersion;
349
+ if (data.batteryLevel !== undefined)
350
+ updateData.batteryLevel = data.batteryLevel;
351
+ if (data.storageUsed !== undefined)
352
+ updateData.storageUsed = data.storageUsed;
353
+ if (data.storageTotal !== undefined)
354
+ updateData.storageTotal = data.storageTotal;
355
+ if (data.lastHeartbeat !== undefined)
356
+ updateData.lastHeartbeat = data.lastHeartbeat;
357
+ if (data.lastSync !== undefined) updateData.lastSync = data.lastSync;
358
+ if (data.installedApps !== undefined)
359
+ updateData.installedApps = data.installedApps;
360
+ if (data.tags !== undefined) updateData.tags = data.tags;
361
+ if (data.metadata !== undefined) updateData.metadata = data.metadata;
362
+
363
+ if (data.location) {
364
+ updateData.latitude = data.location.latitude.toString();
365
+ updateData.longitude = data.location.longitude.toString();
366
+ updateData.locationTimestamp = data.location.timestamp;
367
+ }
368
+
369
+ await (db as any)
370
+ .update(devices)
371
+ .set(updateData)
372
+ .where(eq(devices.id, id));
373
+
374
+ return this.findDevice(id) as Promise<Device>;
375
+ },
376
+
377
+ async deleteDevice(id: string): Promise<void> {
378
+ await (db as any).delete(devices).where(eq(devices.id, id));
379
+ },
380
+
381
+ async countDevices(filter?: DeviceFilter): Promise<number> {
382
+ const result = await this.listDevices({ ...filter, limit: 0 });
383
+ return result.total;
384
+ },
385
+
386
+ // ============================================
387
+ // Policy Methods
388
+ // ============================================
389
+
390
+ async findPolicy(id: string): Promise<Policy | null> {
391
+ const result = await (db as any)
392
+ .select()
393
+ .from(policies)
394
+ .where(eq(policies.id, id))
395
+ .limit(1);
396
+ return result[0] ? toPolicy(result[0]) : null;
397
+ },
398
+
399
+ async findDefaultPolicy(): Promise<Policy | null> {
400
+ const result = await (db as any)
401
+ .select()
402
+ .from(policies)
403
+ .where(eq(policies.isDefault, true))
404
+ .limit(1);
405
+ return result[0] ? toPolicy(result[0]) : null;
406
+ },
407
+
408
+ async listPolicies(): Promise<Policy[]> {
409
+ const result = await (db as any)
410
+ .select()
411
+ .from(policies)
412
+ .orderBy(desc(policies.createdAt));
413
+ return result.map(toPolicy);
414
+ },
415
+
416
+ async createPolicy(data: CreatePolicyInput): Promise<Policy> {
417
+ const id = generateId();
418
+ const now = new Date();
419
+
420
+ const policyData = {
421
+ id,
422
+ name: data.name,
423
+ description: data.description ?? null,
424
+ isDefault: data.isDefault ?? false,
425
+ settings: data.settings,
426
+ createdAt: now,
427
+ updatedAt: now,
428
+ };
429
+
430
+ await (db as any).insert(policies).values(policyData);
431
+
432
+ return this.findPolicy(id) as Promise<Policy>;
433
+ },
434
+
435
+ async updatePolicy(id: string, data: UpdatePolicyInput): Promise<Policy> {
436
+ const updateData: Record<string, unknown> = {
437
+ updatedAt: new Date(),
438
+ };
439
+
440
+ if (data.name !== undefined) updateData.name = data.name;
441
+ if (data.description !== undefined)
442
+ updateData.description = data.description;
443
+ if (data.isDefault !== undefined) updateData.isDefault = data.isDefault;
444
+ if (data.settings !== undefined) updateData.settings = data.settings;
445
+
446
+ await (db as any)
447
+ .update(policies)
448
+ .set(updateData)
449
+ .where(eq(policies.id, id));
450
+
451
+ return this.findPolicy(id) as Promise<Policy>;
452
+ },
453
+
454
+ async deletePolicy(id: string): Promise<void> {
455
+ await (db as any).delete(policies).where(eq(policies.id, id));
456
+ },
457
+
458
+ // ============================================
459
+ // Application Methods
460
+ // ============================================
461
+
462
+ async findApplication(id: string): Promise<Application | null> {
463
+ const result = await (db as any)
464
+ .select()
465
+ .from(applications)
466
+ .where(eq(applications.id, id))
467
+ .limit(1);
468
+ return result[0] ? toApplication(result[0]) : null;
469
+ },
470
+
471
+ async findApplicationByPackage(
472
+ packageName: string,
473
+ version?: string
474
+ ): Promise<Application | null> {
475
+ let query = (db as any)
476
+ .select()
477
+ .from(applications)
478
+ .where(eq(applications.packageName, packageName));
479
+
480
+ if (version) {
481
+ query = query.where(eq(applications.version, version));
482
+ }
483
+
484
+ const result = await query.orderBy(desc(applications.versionCode)).limit(1);
485
+ return result[0] ? toApplication(result[0]) : null;
486
+ },
487
+
488
+ async listApplications(activeOnly?: boolean): Promise<Application[]> {
489
+ let query = (db as any).select().from(applications);
490
+
491
+ if (activeOnly) {
492
+ query = query.where(eq(applications.isActive, true));
493
+ }
494
+
495
+ const result = await query.orderBy(desc(applications.createdAt));
496
+ return result.map(toApplication);
497
+ },
498
+
499
+ async createApplication(data: CreateApplicationInput): Promise<Application> {
500
+ const id = generateId();
501
+ const now = new Date();
502
+
503
+ const appData = {
504
+ id,
505
+ name: data.name,
506
+ packageName: data.packageName,
507
+ version: data.version,
508
+ versionCode: data.versionCode,
509
+ url: data.url,
510
+ hash: data.hash ?? null,
511
+ size: data.size ?? null,
512
+ minSdkVersion: data.minSdkVersion ?? null,
513
+ showIcon: data.showIcon ?? true,
514
+ runAfterInstall: data.runAfterInstall ?? false,
515
+ runAtBoot: data.runAtBoot ?? false,
516
+ isSystem: data.isSystem ?? false,
517
+ isActive: true,
518
+ metadata: data.metadata ?? null,
519
+ createdAt: now,
520
+ updatedAt: now,
521
+ };
522
+
523
+ await (db as any).insert(applications).values(appData);
524
+
525
+ return this.findApplication(id) as Promise<Application>;
526
+ },
527
+
528
+ async updateApplication(
529
+ id: string,
530
+ data: UpdateApplicationInput
531
+ ): Promise<Application> {
532
+ const updateData: Record<string, unknown> = {
533
+ updatedAt: new Date(),
534
+ };
535
+
536
+ if (data.name !== undefined) updateData.name = data.name;
537
+ if (data.version !== undefined) updateData.version = data.version;
538
+ if (data.versionCode !== undefined)
539
+ updateData.versionCode = data.versionCode;
540
+ if (data.url !== undefined) updateData.url = data.url;
541
+ if (data.hash !== undefined) updateData.hash = data.hash;
542
+ if (data.size !== undefined) updateData.size = data.size;
543
+ if (data.minSdkVersion !== undefined)
544
+ updateData.minSdkVersion = data.minSdkVersion;
545
+ if (data.showIcon !== undefined) updateData.showIcon = data.showIcon;
546
+ if (data.runAfterInstall !== undefined)
547
+ updateData.runAfterInstall = data.runAfterInstall;
548
+ if (data.runAtBoot !== undefined) updateData.runAtBoot = data.runAtBoot;
549
+ if (data.isActive !== undefined) updateData.isActive = data.isActive;
550
+ if (data.metadata !== undefined) updateData.metadata = data.metadata;
551
+
552
+ await (db as any)
553
+ .update(applications)
554
+ .set(updateData)
555
+ .where(eq(applications.id, id));
556
+
557
+ return this.findApplication(id) as Promise<Application>;
558
+ },
559
+
560
+ async deleteApplication(id: string): Promise<void> {
561
+ await (db as any).delete(applications).where(eq(applications.id, id));
562
+ },
563
+
564
+ // ============================================
565
+ // Command Methods
566
+ // ============================================
567
+
568
+ async findCommand(id: string): Promise<Command | null> {
569
+ const result = await (db as any)
570
+ .select()
571
+ .from(commands)
572
+ .where(eq(commands.id, id))
573
+ .limit(1);
574
+ return result[0] ? toCommand(result[0]) : null;
575
+ },
576
+
577
+ async listCommands(filter?: CommandFilter): Promise<Command[]> {
578
+ let query = (db as any).select().from(commands);
579
+
580
+ const conditions: (SQL | undefined)[] = [];
581
+
582
+ if (filter?.deviceId) {
583
+ conditions.push(eq(commands.deviceId, filter.deviceId));
584
+ }
585
+
586
+ if (filter?.status) {
587
+ if (Array.isArray(filter.status)) {
588
+ conditions.push(inArray(commands.status, filter.status));
589
+ } else {
590
+ conditions.push(eq(commands.status, filter.status));
591
+ }
592
+ }
593
+
594
+ if (filter?.type) {
595
+ if (Array.isArray(filter.type)) {
596
+ conditions.push(inArray(commands.type, filter.type));
597
+ } else {
598
+ conditions.push(eq(commands.type, filter.type));
599
+ }
600
+ }
601
+
602
+ if (conditions.length > 0) {
603
+ query = query.where(and(...conditions));
604
+ }
605
+
606
+ const limit = filter?.limit ?? 100;
607
+ const offset = filter?.offset ?? 0;
608
+
609
+ const result = await query
610
+ .orderBy(desc(commands.createdAt))
611
+ .limit(limit)
612
+ .offset(offset);
613
+
614
+ return result.map(toCommand);
615
+ },
616
+
617
+ async createCommand(data: SendCommandInput): Promise<Command> {
618
+ const id = generateId();
619
+ const now = new Date();
620
+
621
+ const commandData = {
622
+ id,
623
+ deviceId: data.deviceId,
624
+ type: data.type,
625
+ payload: data.payload ?? null,
626
+ status: 'pending' as const,
627
+ createdAt: now,
628
+ };
629
+
630
+ await (db as any).insert(commands).values(commandData);
631
+
632
+ return this.findCommand(id) as Promise<Command>;
633
+ },
634
+
635
+ async updateCommand(id: string, data: Partial<Command>): Promise<Command> {
636
+ const updateData: Record<string, unknown> = {};
637
+
638
+ if (data.status !== undefined) updateData.status = data.status;
639
+ if (data.result !== undefined) updateData.result = data.result;
640
+ if (data.error !== undefined) updateData.error = data.error;
641
+ if (data.sentAt !== undefined) updateData.sentAt = data.sentAt;
642
+ if (data.acknowledgedAt !== undefined)
643
+ updateData.acknowledgedAt = data.acknowledgedAt;
644
+ if (data.completedAt !== undefined)
645
+ updateData.completedAt = data.completedAt;
646
+
647
+ await (db as any)
648
+ .update(commands)
649
+ .set(updateData)
650
+ .where(eq(commands.id, id));
651
+
652
+ return this.findCommand(id) as Promise<Command>;
653
+ },
654
+
655
+ async getPendingCommands(deviceId: string): Promise<Command[]> {
656
+ return this.listCommands({
657
+ deviceId,
658
+ status: ['pending', 'sent'],
659
+ });
660
+ },
661
+
662
+ // ============================================
663
+ // Event Methods
664
+ // ============================================
665
+
666
+ async createEvent(
667
+ data: Omit<MDMEvent, 'id' | 'createdAt'>
668
+ ): Promise<MDMEvent> {
669
+ const id = generateId();
670
+ const now = new Date();
671
+
672
+ const eventData = {
673
+ id,
674
+ deviceId: data.deviceId,
675
+ type: data.type,
676
+ payload: data.payload,
677
+ createdAt: now,
678
+ };
679
+
680
+ await (db as any).insert(events).values(eventData);
681
+
682
+ return {
683
+ ...eventData,
684
+ createdAt: now,
685
+ };
686
+ },
687
+
688
+ async listEvents(filter?: EventFilter): Promise<MDMEvent[]> {
689
+ let query = (db as any).select().from(events);
690
+
691
+ const conditions: (SQL | undefined)[] = [];
692
+
693
+ if (filter?.deviceId) {
694
+ conditions.push(eq(events.deviceId, filter.deviceId));
695
+ }
696
+
697
+ if (filter?.type) {
698
+ if (Array.isArray(filter.type)) {
699
+ conditions.push(inArray(events.type, filter.type));
700
+ } else {
701
+ conditions.push(eq(events.type, filter.type));
702
+ }
703
+ }
704
+
705
+ if (conditions.length > 0) {
706
+ query = query.where(and(...conditions));
707
+ }
708
+
709
+ const limit = filter?.limit ?? 100;
710
+ const offset = filter?.offset ?? 0;
711
+
712
+ const result = await query
713
+ .orderBy(desc(events.createdAt))
714
+ .limit(limit)
715
+ .offset(offset);
716
+
717
+ return result.map(toEvent);
718
+ },
719
+
720
+ // ============================================
721
+ // Group Methods
722
+ // ============================================
723
+
724
+ async findGroup(id: string): Promise<Group | null> {
725
+ const result = await (db as any)
726
+ .select()
727
+ .from(groups)
728
+ .where(eq(groups.id, id))
729
+ .limit(1);
730
+ return result[0] ? toGroup(result[0]) : null;
731
+ },
732
+
733
+ async listGroups(): Promise<Group[]> {
734
+ const result = await (db as any)
735
+ .select()
736
+ .from(groups)
737
+ .orderBy(groups.name);
738
+ return result.map(toGroup);
739
+ },
740
+
741
+ async createGroup(data: CreateGroupInput): Promise<Group> {
742
+ const id = generateId();
743
+ const now = new Date();
744
+
745
+ const groupData = {
746
+ id,
747
+ name: data.name,
748
+ description: data.description ?? null,
749
+ policyId: data.policyId ?? null,
750
+ parentId: data.parentId ?? null,
751
+ metadata: data.metadata ?? null,
752
+ createdAt: now,
753
+ updatedAt: now,
754
+ };
755
+
756
+ await (db as any).insert(groups).values(groupData);
757
+
758
+ return this.findGroup(id) as Promise<Group>;
759
+ },
760
+
761
+ async updateGroup(id: string, data: UpdateGroupInput): Promise<Group> {
762
+ const updateData: Record<string, unknown> = {
763
+ updatedAt: new Date(),
764
+ };
765
+
766
+ if (data.name !== undefined) updateData.name = data.name;
767
+ if (data.description !== undefined)
768
+ updateData.description = data.description;
769
+ if (data.policyId !== undefined) updateData.policyId = data.policyId;
770
+ if (data.parentId !== undefined) updateData.parentId = data.parentId;
771
+ if (data.metadata !== undefined) updateData.metadata = data.metadata;
772
+
773
+ await (db as any)
774
+ .update(groups)
775
+ .set(updateData)
776
+ .where(eq(groups.id, id));
777
+
778
+ return this.findGroup(id) as Promise<Group>;
779
+ },
780
+
781
+ async deleteGroup(id: string): Promise<void> {
782
+ await (db as any).delete(groups).where(eq(groups.id, id));
783
+ },
784
+
785
+ async listDevicesInGroup(groupId: string): Promise<Device[]> {
786
+ const result = await (db as any)
787
+ .select({ device: devices })
788
+ .from(deviceGroups)
789
+ .innerJoin(devices, eq(deviceGroups.deviceId, devices.id))
790
+ .where(eq(deviceGroups.groupId, groupId));
791
+
792
+ return result.map((r: { device: Record<string, unknown> }) =>
793
+ toDevice(r.device)
794
+ );
795
+ },
796
+
797
+ async addDeviceToGroup(deviceId: string, groupId: string): Promise<void> {
798
+ await (db as any).insert(deviceGroups).values({
799
+ deviceId,
800
+ groupId,
801
+ createdAt: new Date(),
802
+ });
803
+ },
804
+
805
+ async removeDeviceFromGroup(
806
+ deviceId: string,
807
+ groupId: string
808
+ ): Promise<void> {
809
+ await (db as any)
810
+ .delete(deviceGroups)
811
+ .where(
812
+ and(
813
+ eq(deviceGroups.deviceId, deviceId),
814
+ eq(deviceGroups.groupId, groupId)
815
+ )
816
+ );
817
+ },
818
+
819
+ async getDeviceGroups(deviceId: string): Promise<Group[]> {
820
+ const result = await (db as any)
821
+ .select({ group: groups })
822
+ .from(deviceGroups)
823
+ .innerJoin(groups, eq(deviceGroups.groupId, groups.id))
824
+ .where(eq(deviceGroups.deviceId, deviceId));
825
+
826
+ return result.map((r: { group: Record<string, unknown> }) =>
827
+ toGroup(r.group)
828
+ );
829
+ },
830
+
831
+ // ============================================
832
+ // Push Token Methods
833
+ // ============================================
834
+
835
+ async findPushToken(
836
+ deviceId: string,
837
+ provider: string
838
+ ): Promise<PushToken | null> {
839
+ const result = await (db as any)
840
+ .select()
841
+ .from(pushTokens)
842
+ .where(
843
+ and(
844
+ eq(pushTokens.deviceId, deviceId),
845
+ eq(pushTokens.provider, provider as any)
846
+ )
847
+ )
848
+ .limit(1);
849
+ return result[0] ? toPushToken(result[0]) : null;
850
+ },
851
+
852
+ async upsertPushToken(data: RegisterPushTokenInput): Promise<PushToken> {
853
+ const existing = await this.findPushToken(data.deviceId, data.provider);
854
+ const now = new Date();
855
+
856
+ if (existing) {
857
+ await (db as any)
858
+ .update(pushTokens)
859
+ .set({
860
+ token: data.token,
861
+ isActive: true,
862
+ updatedAt: now,
863
+ })
864
+ .where(eq(pushTokens.id, existing.id));
865
+
866
+ return this.findPushToken(data.deviceId, data.provider) as Promise<PushToken>;
867
+ }
868
+
869
+ const id = generateId();
870
+ await (db as any).insert(pushTokens).values({
871
+ id,
872
+ deviceId: data.deviceId,
873
+ provider: data.provider,
874
+ token: data.token,
875
+ isActive: true,
876
+ createdAt: now,
877
+ updatedAt: now,
878
+ });
879
+
880
+ return this.findPushToken(data.deviceId, data.provider) as Promise<PushToken>;
881
+ },
882
+
883
+ async deletePushToken(deviceId: string, provider?: string): Promise<void> {
884
+ if (provider) {
885
+ await (db as any)
886
+ .delete(pushTokens)
887
+ .where(
888
+ and(
889
+ eq(pushTokens.deviceId, deviceId),
890
+ eq(pushTokens.provider, provider as any)
891
+ )
892
+ );
893
+ } else {
894
+ await (db as any)
895
+ .delete(pushTokens)
896
+ .where(eq(pushTokens.deviceId, deviceId));
897
+ }
898
+ },
899
+
900
+ // ============================================
901
+ // Transaction Support
902
+ // ============================================
903
+
904
+ async transaction<T>(fn: () => Promise<T>): Promise<T> {
905
+ return db.transaction(async () => {
906
+ return fn();
907
+ });
908
+ },
909
+ };
910
+ }
911
+
912
+ // Re-export schema utilities
913
+ export { DEFAULT_TABLE_PREFIX } from './schema';
914
+ export type { SchemaOptions } from './schema';