Skip to content

Commit e3e790c

Browse files
feat: refactor database schema and loaders for improved GraphQL integration
1 parent 91d5e2c commit e3e790c

21 files changed

+374
-119
lines changed

server/database/schema/apiKey.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { relations } from 'drizzle-orm'
2-
import { boolean, jsonb, pgTable, text, uuid } from 'drizzle-orm/pg-core'
2+
import { boolean, pgTable, text, uuid } from 'drizzle-orm/pg-core'
33
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
4-
import { customTimestamp, uuidv7Generator } from '../shared'
4+
import { customJsonb, customTimestamp, uuidv7Generator } from '../shared'
55
import { app } from './app'
66

77
export const apiKey = pgTable('apiKey', {
88
id: uuid().primaryKey().$defaultFn(uuidv7Generator),
99
appId: uuid().notNull().references(() => app.id, { onDelete: 'cascade' }),
1010
name: text().notNull(),
1111
key: text().notNull().unique(),
12-
permissions: jsonb(),
12+
permissions: customJsonb(),
1313
isActive: boolean().default(true),
1414
lastUsedAt: customTimestamp(),
1515
expiresAt: customTimestamp(),

server/database/schema/deliveryLog.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { relations } from 'drizzle-orm'
2-
import { integer, jsonb, pgTable, text, unique, uuid } from 'drizzle-orm/pg-core'
2+
import { integer, pgTable, text, unique, uuid } from 'drizzle-orm/pg-core'
33
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
4-
import { customTimestamp, uuidv7Generator } from '../shared'
4+
import { customJsonb, customTimestamp, uuidv7Generator } from '../shared'
55
import { device } from './device'
66
import { deliveryStatusEnum } from './enums'
77
import { notification } from './notification'
@@ -11,7 +11,7 @@ export const deliveryLog = pgTable('deliveryLog', {
1111
notificationId: uuid().notNull().references(() => notification.id, { onDelete: 'cascade' }),
1212
deviceId: uuid().notNull().references(() => device.id, { onDelete: 'cascade' }),
1313
status: deliveryStatusEnum().notNull(),
14-
providerResponse: jsonb(),
14+
providerResponse: customJsonb(),
1515
errorMessage: text(),
1616
attemptCount: integer().default(1),
1717
sentAt: customTimestamp(),

server/database/schema/device.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { relations } from 'drizzle-orm'
2-
import { jsonb, pgTable, text, unique, uuid } from 'drizzle-orm/pg-core'
2+
import { pgTable, text, unique, uuid } from 'drizzle-orm/pg-core'
33
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
4-
import { customTimestamp, uuidv7Generator } from '../shared'
4+
import { customJsonb, customTimestamp, uuidv7Generator } from '../shared'
55
import { app } from './app'
66
import { deliveryLog } from './deliveryLog'
77
import { categoryEnum, deviceStatusEnum, platformEnum } from './enums'
@@ -14,7 +14,7 @@ export const device = pgTable('device', {
1414
platform: platformEnum().notNull(),
1515
userId: text(),
1616
status: deviceStatusEnum().default('ACTIVE').notNull(),
17-
metadata: jsonb(),
17+
metadata: customJsonb(),
1818
lastSeenAt: customTimestamp(),
1919
createdAt: customTimestamp().defaultNow().notNull(),
2020
updatedAt: customTimestamp().defaultNow().notNull(),

server/database/schema/notification.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { relations } from 'drizzle-orm'
2-
import { integer, jsonb, pgTable, text, uuid } from 'drizzle-orm/pg-core'
2+
import { integer, pgTable, text, uuid } from 'drizzle-orm/pg-core'
33
import { createInsertSchema, createSelectSchema } from 'drizzle-zod'
4-
import { customTimestamp, uuidv7Generator } from '../shared'
4+
import { customJsonb, customTimestamp, uuidv7Generator } from '../shared'
55
import { app } from './app'
66
import { deliveryLog } from './deliveryLog'
77
import { notificationStatusEnum } from './enums'
@@ -11,15 +11,15 @@ export const notification = pgTable('notification', {
1111
appId: uuid().notNull().references(() => app.id, { onDelete: 'cascade' }),
1212
title: text().notNull(),
1313
body: text().notNull(),
14-
data: jsonb(),
14+
data: customJsonb(),
1515
badge: integer(),
1616
sound: text(),
1717
clickAction: text(),
1818
icon: text(),
1919
image: text(),
2020
imageUrl: text(),
21-
targetDevices: jsonb(),
22-
platforms: jsonb(),
21+
targetDevices: customJsonb(),
22+
platforms: customJsonb(),
2323
scheduledAt: customTimestamp(),
2424
expiresAt: customTimestamp(),
2525
status: notificationStatusEnum().default('PENDING').notNull(),

server/database/shared.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import { timestamp } from 'drizzle-orm/pg-core'
1+
import { jsonb, timestamp } from 'drizzle-orm/pg-core'
22
import { v7 as uuidv7 } from 'uuid'
33

4+
// JSON type for GraphQL compatibility
5+
export type Json = Record<string, unknown> | unknown[] | string | number | boolean | null
6+
7+
// Custom jsonb that returns Json type for GraphQL compatibility
8+
export const customJsonb = () => jsonb().$type<Json>()
9+
410
// Custom timestamp that returns string for GraphQL compatibility
511
export const customTimestamp = () => timestamp({ mode: 'string', precision: 3, withTimezone: true })
612

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
type ApiKey {
2+
id: ID!
3+
appId: ID!
4+
app: App
5+
name: String!
6+
key: String!
7+
permissions: JSON
8+
isActive: Boolean
9+
lastUsedAt: Timestamp
10+
expiresAt: Timestamp
11+
12+
# Timestamps
13+
createdAt: Timestamp!
14+
updatedAt: Timestamp!
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineField } from 'nitro-graphql/utils/define'
2+
3+
export const apiKeyFieldsResolver = defineField({
4+
ApiKey: {
5+
app: {
6+
resolve: async (parent, _args, { context }) => {
7+
const { dataloaders } = context
8+
return await dataloaders.appLoader.load(parent.appId)
9+
},
10+
},
11+
},
12+
})

server/graphql/apps/app.graphql

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ type App {
55
description: String
66
isActive: Boolean!
77
apiKey: String!
8-
8+
99
# Push Provider Configuration
1010
fcmProjectId: String
1111
fcmServiceAccount: String
@@ -16,10 +16,15 @@ type App {
1616
vapidSubject: String
1717
vapidPublicKey: String
1818
vapidPrivateKey: String
19-
19+
20+
# Relations
21+
devices: [Device!]
22+
notifications: [Notification!]
23+
apiKeys: [ApiKey!]
24+
2025
# Statistics
2126
stats: AppStats
22-
27+
2328
# Timestamps
2429
createdAt: Timestamp!
2530
updatedAt: Timestamp!
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { and, eq, gte, sql } from 'drizzle-orm'
2+
import { defineField } from 'nitro-graphql/utils/define'
3+
4+
export const appFieldsResolver = defineField({
5+
App: {
6+
devices: {
7+
resolve: async (parent, _args, { context }) => {
8+
const { dataloaders } = context
9+
return await dataloaders.devicesByAppLoader.load(parent.id)
10+
},
11+
},
12+
13+
notifications: {
14+
resolve: async (parent, _args, { context }) => {
15+
const { dataloaders } = context
16+
return await dataloaders.notificationsByAppLoader.load(parent.id)
17+
},
18+
},
19+
20+
apiKeys: {
21+
resolve: async (parent, _args, { context }) => {
22+
const { dataloaders } = context
23+
return await dataloaders.apiKeysByAppLoader.load(parent.id)
24+
},
25+
},
26+
27+
stats: {
28+
resolve: async (parent, _args, { context }) => {
29+
const { useDatabase, tables } = context
30+
const db = useDatabase()
31+
32+
const today = new Date()
33+
today.setHours(0, 0, 0, 0)
34+
35+
// Get total devices
36+
const totalDevicesResult = await db
37+
.select({ count: sql<number>`count(*)` })
38+
.from(tables.device)
39+
.where(eq(tables.device.appId, parent.id))
40+
41+
// Get active devices
42+
const activeDevicesResult = await db
43+
.select({ count: sql<number>`count(*)` })
44+
.from(tables.device)
45+
.where(
46+
and(
47+
eq(tables.device.appId, parent.id),
48+
eq(tables.device.status, 'ACTIVE'),
49+
),
50+
)
51+
52+
// Get new devices today
53+
const newDevicesTodayResult = await db
54+
.select({ count: sql<number>`count(*)` })
55+
.from(tables.device)
56+
.where(
57+
and(
58+
eq(tables.device.appId, parent.id),
59+
gte(tables.device.createdAt, today.toISOString()),
60+
),
61+
)
62+
63+
// Get sent notifications today
64+
const sentTodayResult = await db
65+
.select({ count: sql<number>`count(*)` })
66+
.from(tables.notification)
67+
.where(
68+
and(
69+
eq(tables.notification.appId, parent.id),
70+
gte(tables.notification.sentAt, today.toISOString()),
71+
),
72+
)
73+
74+
// Get delivery stats for rate calculation
75+
const deliveryStatsResult = await db
76+
.select({
77+
totalSent: sql<number>`sum(${tables.notification.totalSent})`,
78+
totalDelivered: sql<number>`sum(${tables.notification.totalDelivered})`,
79+
})
80+
.from(tables.notification)
81+
.where(eq(tables.notification.appId, parent.id))
82+
83+
const totalSent = deliveryStatsResult[0]?.totalSent || 0
84+
const totalDelivered = deliveryStatsResult[0]?.totalDelivered || 0
85+
const deliveryRate = totalSent > 0 ? (totalDelivered / totalSent) * 100 : 0
86+
87+
return {
88+
totalDevices: Number(totalDevicesResult[0]?.count) || 0,
89+
activeDevices: Number(activeDevicesResult[0]?.count) || 0,
90+
newDevicesToday: Number(newDevicesTodayResult[0]?.count) || 0,
91+
sentToday: Number(sentTodayResult[0]?.count) || 0,
92+
deliveryRate,
93+
apiCalls: 0, // TODO: Implement API call tracking
94+
}
95+
},
96+
},
97+
},
98+
})

server/graphql/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as tables from '~~/server/database/schema'
2-
import { createDataLoaders } from '~~/server/utils/dataloaders'
2+
import { createDataLoaders } from '~~/server/graphql/loaders'
33
import { useDatabase } from '~~/server/utils/useDatabase'
44

55
export default defineGraphQLConfig({

0 commit comments

Comments
 (0)