@modern-admin/system-prisma 0.1.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/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/stores/ai-task-store.d.ts +43 -0
- package/dist/stores/ai-task-store.d.ts.map +1 -0
- package/dist/stores/ai-task-store.js +108 -0
- package/dist/stores/ai-task-store.js.map +1 -0
- package/dist/stores/cache-store.d.ts +30 -0
- package/dist/stores/cache-store.d.ts.map +1 -0
- package/dist/stores/cache-store.js +61 -0
- package/dist/stores/cache-store.js.map +1 -0
- package/dist/stores/config-store.d.ts +20 -0
- package/dist/stores/config-store.d.ts.map +1 -0
- package/dist/stores/config-store.js +61 -0
- package/dist/stores/config-store.js.map +1 -0
- package/dist/stores/history-store.d.ts +33 -0
- package/dist/stores/history-store.d.ts.map +1 -0
- package/dist/stores/history-store.js +57 -0
- package/dist/stores/history-store.js.map +1 -0
- package/dist/stores/log-store.d.ts +23 -0
- package/dist/stores/log-store.d.ts.map +1 -0
- package/dist/stores/log-store.js +65 -0
- package/dist/stores/log-store.js.map +1 -0
- package/dist/stores/webhook-store.d.ts +43 -0
- package/dist/stores/webhook-store.d.ts.map +1 -0
- package/dist/stores/webhook-store.js +111 -0
- package/dist/stores/webhook-store.js.map +1 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
- package/src/index.ts +88 -0
- package/src/stores/ai-task-store.ts +154 -0
- package/src/stores/cache-store.ts +79 -0
- package/src/stores/config-store.ts +80 -0
- package/src/stores/history-store.ts +84 -0
- package/src/stores/log-store.ts +77 -0
- package/src/stores/webhook-store.ts +150 -0
- package/src/types.ts +70 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { CacheEntry, ICacheStore } from '@modern-admin/core'
|
|
2
|
+
import type { PrismaDelegate } from '../types.js'
|
|
3
|
+
|
|
4
|
+
interface CacheRow {
|
|
5
|
+
key: string
|
|
6
|
+
value: unknown
|
|
7
|
+
tags: unknown
|
|
8
|
+
expiresAt: Date | null
|
|
9
|
+
createdAt: Date
|
|
10
|
+
updatedAt: Date
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const rowToEntry = (row: CacheRow): CacheEntry => ({
|
|
14
|
+
key: row.key,
|
|
15
|
+
value: row.value,
|
|
16
|
+
tags: Array.isArray(row.tags) ? (row.tags as string[]) : [],
|
|
17
|
+
expiresAt: row.expiresAt ? row.expiresAt.toISOString() : null,
|
|
18
|
+
createdAt: row.createdAt.toISOString(),
|
|
19
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export class PrismaCacheStore implements ICacheStore {
|
|
23
|
+
constructor(private readonly delegate: PrismaDelegate<CacheRow>) {}
|
|
24
|
+
|
|
25
|
+
async get(key: string): Promise<CacheEntry | null> {
|
|
26
|
+
const row = await this.delegate.findUnique({ where: { key } })
|
|
27
|
+
if (!row) return null
|
|
28
|
+
if (row.expiresAt && row.expiresAt.getTime() < Date.now()) {
|
|
29
|
+
await this.delegate.delete({ where: { key } }).catch(() => undefined)
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
return rowToEntry(row)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async set(
|
|
36
|
+
key: string,
|
|
37
|
+
value: unknown,
|
|
38
|
+
options: { ttlMs?: number; tags?: string[] } = {},
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
const expiresAt = options.ttlMs ? new Date(Date.now() + options.ttlMs) : null
|
|
41
|
+
const tags = options.tags ?? []
|
|
42
|
+
await this.delegate.upsert({
|
|
43
|
+
where: { key },
|
|
44
|
+
create: { key, value: value ?? null, tags, expiresAt },
|
|
45
|
+
update: { value: value ?? null, tags, expiresAt },
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async delete(key: string): Promise<void> {
|
|
50
|
+
await this.delegate.delete({ where: { key } }).catch(() => undefined)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Tag invalidation walks the table because Prisma's portable `Json` field
|
|
55
|
+
* doesn't support array-contains across all DB engines. For high-volume
|
|
56
|
+
* caches use a Postgres-specific implementation (`tags String[]`) or
|
|
57
|
+
* Redis. Here correctness > throughput by design.
|
|
58
|
+
*/
|
|
59
|
+
async invalidateTags(tags: string[]): Promise<number> {
|
|
60
|
+
if (!tags.length) return 0
|
|
61
|
+
const set = new Set(tags)
|
|
62
|
+
const rows = await this.delegate.findMany({})
|
|
63
|
+
const targets = rows.filter((r) =>
|
|
64
|
+
Array.isArray(r.tags) && (r.tags as string[]).some((t) => set.has(t)),
|
|
65
|
+
)
|
|
66
|
+
if (!targets.length) return 0
|
|
67
|
+
const result = await this.delegate.deleteMany({
|
|
68
|
+
where: { key: { in: targets.map((r) => r.key) } },
|
|
69
|
+
})
|
|
70
|
+
return result.count
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async prune(): Promise<number> {
|
|
74
|
+
const result = await this.delegate.deleteMany({
|
|
75
|
+
where: { expiresAt: { lt: new Date() } },
|
|
76
|
+
})
|
|
77
|
+
return result.count
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type ConfigEntry, type ConfigScope, type IConfigStore, uuidv7 } from '@modern-admin/core'
|
|
2
|
+
import type { PrismaDelegate } from '../types.js'
|
|
3
|
+
|
|
4
|
+
interface ConfigRow {
|
|
5
|
+
id: string
|
|
6
|
+
scope: string
|
|
7
|
+
scopeId: string
|
|
8
|
+
key: string
|
|
9
|
+
value: unknown
|
|
10
|
+
updatedAt: Date
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const GLOBAL_SCOPE_ID = ''
|
|
14
|
+
const encodeScopeId = (scopeId: string | null): string => scopeId ?? GLOBAL_SCOPE_ID
|
|
15
|
+
const decodeScopeId = (scopeId: string): string | null => scopeId === GLOBAL_SCOPE_ID ? null : scopeId
|
|
16
|
+
|
|
17
|
+
const rowToEntry = (row: ConfigRow): ConfigEntry => ({
|
|
18
|
+
scope: row.scope as ConfigScope,
|
|
19
|
+
scopeId: decodeScopeId(row.scopeId),
|
|
20
|
+
key: row.key,
|
|
21
|
+
value: row.value,
|
|
22
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export class PrismaConfigStore implements IConfigStore {
|
|
26
|
+
constructor(private readonly delegate: PrismaDelegate<ConfigRow>) {
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async get(scope: ConfigScope, scopeId: string | null, key: string): Promise<unknown> {
|
|
30
|
+
// findFirst instead of findUnique: tolerates accidental duplicates and
|
|
31
|
+
// returns the most-recently-updated row rather than throwing.
|
|
32
|
+
const row = await this.delegate.findFirst({
|
|
33
|
+
where: { scope, scopeId: encodeScopeId(scopeId), key },
|
|
34
|
+
orderBy: { updatedAt: 'desc' },
|
|
35
|
+
})
|
|
36
|
+
return row?.value
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async set(
|
|
40
|
+
scope: ConfigScope,
|
|
41
|
+
scopeId: string | null,
|
|
42
|
+
key: string,
|
|
43
|
+
value: unknown,
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
const encodedScopeId = encodeScopeId(scopeId)
|
|
46
|
+
// Use findFirst + update-by-id / create instead of upsert with a
|
|
47
|
+
// compound unique key. Prisma 7 + PrismaPg adapter can misidentify
|
|
48
|
+
// compound-key upserts and issue a second INSERT rather than an UPDATE,
|
|
49
|
+
// which violates the unique constraint. Anchoring the update on the
|
|
50
|
+
// surrogate id is always safe.
|
|
51
|
+
const existing = await this.delegate.findFirst({
|
|
52
|
+
where: { scope, scopeId: encodedScopeId, key },
|
|
53
|
+
})
|
|
54
|
+
if (existing) {
|
|
55
|
+
await this.delegate.update({
|
|
56
|
+
where: { id: existing.id },
|
|
57
|
+
data: { value: value ?? null },
|
|
58
|
+
})
|
|
59
|
+
} else {
|
|
60
|
+
await this.delegate.create({
|
|
61
|
+
data: { id: uuidv7(), scope, scopeId: encodedScopeId, key, value: value ?? null },
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async delete(scope: ConfigScope, scopeId: string | null, key: string): Promise<void> {
|
|
67
|
+
// deleteMany avoids "record not found" and also cleans up any duplicates.
|
|
68
|
+
await this.delegate
|
|
69
|
+
.deleteMany({ where: { scope, scopeId: encodeScopeId(scopeId), key } })
|
|
70
|
+
.catch(() => undefined)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async list(scope: ConfigScope, scopeId: string | null): Promise<ConfigEntry[]> {
|
|
74
|
+
const rows = await this.delegate.findMany({
|
|
75
|
+
where: { scope, scopeId: encodeScopeId(scopeId) },
|
|
76
|
+
orderBy: { key: 'asc' },
|
|
77
|
+
})
|
|
78
|
+
return rows.map(rowToEntry)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { uuidv7, type HistoryEntry, type HistoryOp, type IHistoryStore } from '@modern-admin/core'
|
|
2
|
+
import type { PrismaDelegate } from '../types.js'
|
|
3
|
+
|
|
4
|
+
interface HistoryRow {
|
|
5
|
+
id: string
|
|
6
|
+
resourceId: string
|
|
7
|
+
recordId: string
|
|
8
|
+
op: string
|
|
9
|
+
userId: string | null
|
|
10
|
+
snapshot: unknown
|
|
11
|
+
snapshotBefore: unknown
|
|
12
|
+
diff: unknown
|
|
13
|
+
createdAt: Date
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const rowToEntry = (row: HistoryRow): HistoryEntry => ({
|
|
17
|
+
id: row.id,
|
|
18
|
+
resourceId: row.resourceId,
|
|
19
|
+
recordId: row.recordId,
|
|
20
|
+
op: row.op as HistoryOp,
|
|
21
|
+
...(row.userId !== null ? { userId: row.userId } : {}),
|
|
22
|
+
snapshot: (row.snapshot as Record<string, unknown>) ?? {},
|
|
23
|
+
...(row.snapshotBefore !== null && row.snapshotBefore !== undefined
|
|
24
|
+
? { snapshotBefore: row.snapshotBefore as Record<string, unknown> }
|
|
25
|
+
: {}),
|
|
26
|
+
createdAt: row.createdAt.toISOString(),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export class PrismaHistoryStore implements IHistoryStore {
|
|
30
|
+
constructor(private readonly delegate: PrismaDelegate<HistoryRow>) {}
|
|
31
|
+
|
|
32
|
+
async append(input: {
|
|
33
|
+
resourceId: string
|
|
34
|
+
recordId: string
|
|
35
|
+
op: HistoryOp
|
|
36
|
+
userId?: string
|
|
37
|
+
snapshot: Record<string, unknown>
|
|
38
|
+
snapshotBefore?: Record<string, unknown>
|
|
39
|
+
}): Promise<HistoryEntry> {
|
|
40
|
+
const row = await this.delegate.create({
|
|
41
|
+
data: {
|
|
42
|
+
id: uuidv7(),
|
|
43
|
+
resourceId: input.resourceId,
|
|
44
|
+
recordId: input.recordId,
|
|
45
|
+
op: input.op,
|
|
46
|
+
userId: input.userId ?? null,
|
|
47
|
+
snapshot: input.snapshot,
|
|
48
|
+
snapshotBefore: input.snapshotBefore ?? null,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
return rowToEntry(row)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async list(
|
|
55
|
+
resourceId: string,
|
|
56
|
+
recordId: string,
|
|
57
|
+
options: { limit?: number; offset?: number } = {},
|
|
58
|
+
): Promise<HistoryEntry[]> {
|
|
59
|
+
const limit = options.limit ?? 50
|
|
60
|
+
const offset = options.offset ?? 0
|
|
61
|
+
const rows = await this.delegate.findMany({
|
|
62
|
+
where: { resourceId, recordId },
|
|
63
|
+
orderBy: { createdAt: 'desc' },
|
|
64
|
+
take: limit,
|
|
65
|
+
...(offset > 0 ? { skip: offset } : {}),
|
|
66
|
+
})
|
|
67
|
+
return rows.map(rowToEntry)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async get(resourceId: string, recordId: string, revisionId: string): Promise<HistoryEntry | null> {
|
|
71
|
+
const row = await this.delegate.findFirst({
|
|
72
|
+
where: { id: revisionId, resourceId, recordId },
|
|
73
|
+
})
|
|
74
|
+
return row ? rowToEntry(row) : null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async latest(resourceId: string, recordId: string): Promise<HistoryEntry | null> {
|
|
78
|
+
const row = await this.delegate.findFirst({
|
|
79
|
+
where: { resourceId, recordId },
|
|
80
|
+
orderBy: { createdAt: 'desc' },
|
|
81
|
+
})
|
|
82
|
+
return row ? rowToEntry(row) : null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { uuidv7, type ActionLogEntry, type ILogStore, type IQueryableLogStore } from '@modern-admin/core'
|
|
2
|
+
import type { PrismaDelegate } from '../types.js'
|
|
3
|
+
|
|
4
|
+
interface LogRow {
|
|
5
|
+
id: string
|
|
6
|
+
resourceId: string
|
|
7
|
+
action: string
|
|
8
|
+
recordId: string | null
|
|
9
|
+
recordIds: unknown
|
|
10
|
+
userId: string | null
|
|
11
|
+
payload: unknown
|
|
12
|
+
result: unknown
|
|
13
|
+
at: bigint
|
|
14
|
+
createdAt: Date
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const rowToEntry = (row: LogRow): ActionLogEntry => ({
|
|
18
|
+
id: row.id,
|
|
19
|
+
resourceId: row.resourceId,
|
|
20
|
+
action: row.action,
|
|
21
|
+
...(row.recordId !== null ? { recordId: row.recordId } : {}),
|
|
22
|
+
...(Array.isArray(row.recordIds) ? { recordIds: row.recordIds as string[] } : {}),
|
|
23
|
+
...(row.userId !== null ? { userId: row.userId } : {}),
|
|
24
|
+
...(row.payload !== null && row.payload !== undefined
|
|
25
|
+
? { payload: row.payload as Record<string, unknown> }
|
|
26
|
+
: {}),
|
|
27
|
+
...(row.result !== null && row.result !== undefined
|
|
28
|
+
? { result: row.result as Record<string, unknown> }
|
|
29
|
+
: {}),
|
|
30
|
+
at: Number(row.at),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export class PrismaLogStore implements IQueryableLogStore {
|
|
34
|
+
constructor(private readonly delegate: PrismaDelegate<LogRow>) {}
|
|
35
|
+
|
|
36
|
+
async record(entry: ActionLogEntry): Promise<void> {
|
|
37
|
+
await this.delegate.create({
|
|
38
|
+
data: {
|
|
39
|
+
// Prefer the writer-supplied id (UUID v7 from `actionLoggingPlugin`)
|
|
40
|
+
// so React lists keyed on `entry.id` line up with the persisted row.
|
|
41
|
+
id: entry.id ?? uuidv7(),
|
|
42
|
+
resourceId: entry.resourceId,
|
|
43
|
+
action: entry.action,
|
|
44
|
+
recordId: entry.recordId ?? null,
|
|
45
|
+
recordIds: entry.recordIds ?? null,
|
|
46
|
+
userId: entry.userId ?? null,
|
|
47
|
+
payload: entry.payload ?? null,
|
|
48
|
+
result: entry.result ?? null,
|
|
49
|
+
at: BigInt(entry.at),
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async list(filter: Parameters<IQueryableLogStore['list']>[0] = {}): Promise<ActionLogEntry[]> {
|
|
55
|
+
const where: Record<string, unknown> = {}
|
|
56
|
+
if (filter.resourceId) where['resourceId'] = filter.resourceId
|
|
57
|
+
if (filter.recordId) where['recordId'] = filter.recordId
|
|
58
|
+
if (filter.userId) where['userId'] = filter.userId
|
|
59
|
+
if (filter.actions?.length) where['action'] = { in: filter.actions }
|
|
60
|
+
if (filter.from || filter.to) {
|
|
61
|
+
const range: Record<string, bigint> = {}
|
|
62
|
+
if (filter.from) range['gte'] = BigInt(filter.from.getTime())
|
|
63
|
+
if (filter.to) range['lte'] = BigInt(filter.to.getTime())
|
|
64
|
+
where['at'] = range
|
|
65
|
+
}
|
|
66
|
+
const rows = await this.delegate.findMany({
|
|
67
|
+
where,
|
|
68
|
+
orderBy: { at: 'desc' },
|
|
69
|
+
...(filter.limit !== undefined ? { take: filter.limit } : {}),
|
|
70
|
+
...(filter.offset !== undefined ? { skip: filter.offset } : {}),
|
|
71
|
+
})
|
|
72
|
+
return rows.map(rowToEntry)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Narrow type-test: a pure `ILogStore` is enough where readback isn't needed. */
|
|
77
|
+
export type { ILogStore }
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
uuidv7,
|
|
3
|
+
type IWebhookStore,
|
|
4
|
+
type Webhook,
|
|
5
|
+
type WebhookDelivery,
|
|
6
|
+
type WebhookDeliveryStatus,
|
|
7
|
+
type WebhookInput,
|
|
8
|
+
} from '@modern-admin/core'
|
|
9
|
+
import type { PrismaDelegate } from '../types.js'
|
|
10
|
+
|
|
11
|
+
interface WebhookRow {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
url: string
|
|
15
|
+
events: unknown
|
|
16
|
+
resourceId: string | null
|
|
17
|
+
enabled: boolean
|
|
18
|
+
secret: string | null
|
|
19
|
+
headers: unknown
|
|
20
|
+
filters: unknown
|
|
21
|
+
payloadFields: unknown
|
|
22
|
+
createdAt: Date
|
|
23
|
+
updatedAt: Date
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface DeliveryRow {
|
|
27
|
+
id: string
|
|
28
|
+
webhookId: string
|
|
29
|
+
event: string
|
|
30
|
+
payload: unknown
|
|
31
|
+
status: string
|
|
32
|
+
responseStatus: number | null
|
|
33
|
+
responseBody: string | null
|
|
34
|
+
error: string | null
|
|
35
|
+
attempt: number
|
|
36
|
+
createdAt: Date
|
|
37
|
+
deliveredAt: Date | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rowToWebhook = (row: WebhookRow): Webhook => ({
|
|
41
|
+
id: row.id,
|
|
42
|
+
name: row.name,
|
|
43
|
+
url: row.url,
|
|
44
|
+
events: Array.isArray(row.events) ? (row.events as string[]) : [],
|
|
45
|
+
resourceId: row.resourceId,
|
|
46
|
+
enabled: row.enabled,
|
|
47
|
+
...(row.secret !== null ? { secret: row.secret } : {}),
|
|
48
|
+
headers: (row.headers as Record<string, string>) ?? {},
|
|
49
|
+
filters: (row.filters as Record<string, string>) ?? {},
|
|
50
|
+
payloadFields: Array.isArray(row.payloadFields) ? (row.payloadFields as string[]) : [],
|
|
51
|
+
createdAt: row.createdAt.toISOString(),
|
|
52
|
+
updatedAt: row.updatedAt.toISOString(),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const rowToDelivery = (row: DeliveryRow): WebhookDelivery => ({
|
|
56
|
+
id: row.id,
|
|
57
|
+
webhookId: row.webhookId,
|
|
58
|
+
event: row.event,
|
|
59
|
+
payload: (row.payload as Record<string, unknown>) ?? {},
|
|
60
|
+
status: row.status as WebhookDeliveryStatus,
|
|
61
|
+
...(row.responseStatus !== null ? { responseStatus: row.responseStatus } : {}),
|
|
62
|
+
...(row.responseBody !== null ? { responseBody: row.responseBody } : {}),
|
|
63
|
+
...(row.error !== null ? { error: row.error } : {}),
|
|
64
|
+
attempt: row.attempt,
|
|
65
|
+
createdAt: row.createdAt.toISOString(),
|
|
66
|
+
...(row.deliveredAt !== null ? { deliveredAt: row.deliveredAt.toISOString() } : {}),
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
export class PrismaWebhookStore implements IWebhookStore {
|
|
70
|
+
constructor(
|
|
71
|
+
private readonly webhookDelegate: PrismaDelegate<WebhookRow>,
|
|
72
|
+
private readonly deliveryDelegate: PrismaDelegate<DeliveryRow>,
|
|
73
|
+
) {}
|
|
74
|
+
|
|
75
|
+
async list(): Promise<Webhook[]> {
|
|
76
|
+
const rows = await this.webhookDelegate.findMany({ orderBy: { createdAt: 'desc' } })
|
|
77
|
+
return rows.map(rowToWebhook)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async get(id: string): Promise<Webhook | null> {
|
|
81
|
+
const row = await this.webhookDelegate.findUnique({ where: { id } })
|
|
82
|
+
return row ? rowToWebhook(row) : null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async create(input: WebhookInput): Promise<Webhook> {
|
|
86
|
+
const row = await this.webhookDelegate.create({
|
|
87
|
+
data: {
|
|
88
|
+
id: uuidv7(),
|
|
89
|
+
name: input.name,
|
|
90
|
+
url: input.url,
|
|
91
|
+
events: input.events,
|
|
92
|
+
resourceId: input.resourceId ?? null,
|
|
93
|
+
enabled: input.enabled ?? true,
|
|
94
|
+
secret: input.secret ?? null,
|
|
95
|
+
headers: input.headers ?? {},
|
|
96
|
+
filters: input.filters ?? {},
|
|
97
|
+
payloadFields: input.payloadFields ?? [],
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
return rowToWebhook(row)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async update(id: string, patch: Partial<WebhookInput>): Promise<Webhook> {
|
|
104
|
+
const data: Record<string, unknown> = {}
|
|
105
|
+
if (patch.name !== undefined) data['name'] = patch.name
|
|
106
|
+
if (patch.url !== undefined) data['url'] = patch.url
|
|
107
|
+
if (patch.events !== undefined) data['events'] = patch.events
|
|
108
|
+
if (patch.resourceId !== undefined) data['resourceId'] = patch.resourceId ?? null
|
|
109
|
+
if (patch.enabled !== undefined) data['enabled'] = patch.enabled
|
|
110
|
+
if (patch.secret !== undefined) data['secret'] = patch.secret
|
|
111
|
+
if (patch.headers !== undefined) data['headers'] = patch.headers
|
|
112
|
+
if (patch.filters !== undefined) data['filters'] = patch.filters
|
|
113
|
+
if (patch.payloadFields !== undefined) data['payloadFields'] = patch.payloadFields
|
|
114
|
+
const row = await this.webhookDelegate.update({ where: { id }, data })
|
|
115
|
+
return rowToWebhook(row)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async delete(id: string): Promise<void> {
|
|
119
|
+
await this.webhookDelegate.delete({ where: { id } })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async recordDelivery(
|
|
123
|
+
delivery: Omit<WebhookDelivery, 'id' | 'createdAt'>,
|
|
124
|
+
): Promise<WebhookDelivery> {
|
|
125
|
+
const row = await this.deliveryDelegate.create({
|
|
126
|
+
data: {
|
|
127
|
+
id: uuidv7(),
|
|
128
|
+
webhookId: delivery.webhookId,
|
|
129
|
+
event: delivery.event,
|
|
130
|
+
payload: delivery.payload,
|
|
131
|
+
status: delivery.status,
|
|
132
|
+
responseStatus: delivery.responseStatus ?? null,
|
|
133
|
+
responseBody: delivery.responseBody ?? null,
|
|
134
|
+
error: delivery.error ?? null,
|
|
135
|
+
attempt: delivery.attempt,
|
|
136
|
+
deliveredAt: delivery.deliveredAt ? new Date(delivery.deliveredAt) : null,
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
return rowToDelivery(row)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async listDeliveries(webhookId: string, limit = 50): Promise<WebhookDelivery[]> {
|
|
143
|
+
const rows = await this.deliveryDelegate.findMany({
|
|
144
|
+
where: { webhookId },
|
|
145
|
+
orderBy: { createdAt: 'desc' },
|
|
146
|
+
take: limit,
|
|
147
|
+
})
|
|
148
|
+
return rows.map(rowToDelivery)
|
|
149
|
+
}
|
|
150
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural Prisma typings used by the system stores.
|
|
3
|
+
*
|
|
4
|
+
* Prisma generates a fully-typed client per project, so we can't import a
|
|
5
|
+
* concrete `PrismaClient` here. Instead, every store consumes a minimal
|
|
6
|
+
* delegate shape — the same handful of methods Prisma always emits for a
|
|
7
|
+
* model. The host's actual generated client matches this surface
|
|
8
|
+
* structurally, no casts required.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export interface PrismaDelegate<TRow = any> {
|
|
14
|
+
findMany(args?: any): Promise<TRow[]>
|
|
15
|
+
findUnique(args: { where: any }): Promise<TRow | null>
|
|
16
|
+
findFirst(args?: any): Promise<TRow | null>
|
|
17
|
+
create(args: { data: any }): Promise<TRow>
|
|
18
|
+
update(args: { where: any; data: any }): Promise<TRow>
|
|
19
|
+
upsert(args: { where: any; update: any; create: any }): Promise<TRow>
|
|
20
|
+
delete(args: { where: any }): Promise<TRow>
|
|
21
|
+
deleteMany(args?: { where?: any }): Promise<{ count: number }>
|
|
22
|
+
count(args?: any): Promise<number>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whatever `prisma` instance the host injects. We index into it by model
|
|
27
|
+
* name (default: `maLog`, `maWebhook`, …); see `setupPrismaSystem`'s
|
|
28
|
+
* `models` option for renames.
|
|
29
|
+
*
|
|
30
|
+
* Uses `{ [K: string]: any }` (not `Record<string, PrismaDelegate>`) so that
|
|
31
|
+
* the generated `PrismaClient` — which carries utility methods like
|
|
32
|
+
* `$connect`, `$disconnect`, `$transaction` that are NOT `PrismaDelegate` —
|
|
33
|
+
* is directly assignable without a cast. TypeScript only waives the
|
|
34
|
+
* "missing index signature" check when the index value type is `any`; any
|
|
35
|
+
* other type (including `unknown`) still rejects `PrismaClient`. The shape
|
|
36
|
+
* of each delegate is validated at runtime in `resolveDelegate()`.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export type PrismaLike = { [K: string]: any }
|
|
40
|
+
|
|
41
|
+
export const DEFAULT_MODELS = {
|
|
42
|
+
log: 'maLog',
|
|
43
|
+
webhook: 'maWebhook',
|
|
44
|
+
webhookDelivery: 'maWebhookDelivery',
|
|
45
|
+
config: 'maConfig',
|
|
46
|
+
history: 'maHistory',
|
|
47
|
+
aiTask: 'maAiTask',
|
|
48
|
+
aiTaskEvent: 'maAiTaskEvent',
|
|
49
|
+
cache: 'maCache',
|
|
50
|
+
} as const
|
|
51
|
+
|
|
52
|
+
export type ModelKey = keyof typeof DEFAULT_MODELS
|
|
53
|
+
export type ModelOverrides = Partial<Record<ModelKey, string>>
|
|
54
|
+
|
|
55
|
+
export function resolveDelegate(
|
|
56
|
+
prisma: PrismaLike,
|
|
57
|
+
key: ModelKey,
|
|
58
|
+
overrides: ModelOverrides | undefined,
|
|
59
|
+
): PrismaDelegate {
|
|
60
|
+
const name = overrides?.[key] ?? DEFAULT_MODELS[key]
|
|
61
|
+
const delegate = prisma[name] as PrismaDelegate | undefined
|
|
62
|
+
if (!delegate || typeof delegate.findMany !== 'function') {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`[modern-admin/system-prisma] missing delegate "prisma.${name}". ` +
|
|
65
|
+
`Make sure the Modern Admin schema fragment is included in your schema.prisma ` +
|
|
66
|
+
`(see @modern-admin/system-prisma/schema), and that the Prisma client has been generated.`,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
return delegate
|
|
70
|
+
}
|