@manablox/db 0.2.0 → 0.3.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-Cyf_N5K3.d.ts +658 -0
- package/dist/index-rZ24t-Ln.d.ts +4338 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +60 -0
- package/dist/repositories-DYjzuuF6.js +1533 -0
- package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/schema-Bb4p16Yz.js +539 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +2 -0
- package/dist/testing.d.ts +77 -0
- package/dist/testing.js +217 -0
- package/package.json +18 -10
- package/drizzle.config.ts +0 -11
- package/src/bootstrap.ts +0 -13
- package/src/cli/create-db.ts +0 -30
- package/src/cli/migrate.ts +0 -17
- package/src/client.ts +0 -44
- package/src/columns.ts +0 -39
- package/src/errors.ts +0 -50
- package/src/index.ts +0 -19
- package/src/migrate.ts +0 -21
- package/src/pagination.ts +0 -52
- package/src/query.ts +0 -213
- package/src/repositories/asset-usage.ts +0 -166
- package/src/repositories/asset.ts +0 -181
- package/src/repositories/content-type.ts +0 -116
- package/src/repositories/content.ts +0 -811
- package/src/repositories/index.ts +0 -40
- package/src/repositories/menu.ts +0 -235
- package/src/repositories/role.ts +0 -85
- package/src/repositories/space.ts +0 -83
- package/src/repositories/user.ts +0 -280
- package/src/repositories/webhook.ts +0 -46
- package/src/repositories/workflow.ts +0 -306
- package/src/schema/assets.ts +0 -108
- package/src/schema/auth.ts +0 -166
- package/src/schema/content-types.ts +0 -31
- package/src/schema/content.ts +0 -133
- package/src/schema/index.ts +0 -38
- package/src/schema/menus.ts +0 -61
- package/src/schema/relations.ts +0 -64
- package/src/schema/spaces.ts +0 -20
- package/src/schema/webhooks.ts +0 -46
- package/src/schema/workflows.ts +0 -92
- package/src/testing-fixtures.ts +0 -139
- package/src/testing.ts +0 -105
- package/test/asset-usage.test.ts +0 -101
- package/test/menu.test.ts +0 -126
- package/test/publish.test.ts +0 -130
- package/test/query.test.ts +0 -170
- package/test/role.test.ts +0 -81
- package/test/tree.test.ts +0 -188
- package/test/user.test.ts +0 -126
- package/test/webhook.test.ts +0 -48
- package/tsconfig.json +0 -4
- package/vitest.config.ts +0 -10
package/src/repositories/user.ts
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { ManabloxError } from '@manablox/core';
|
|
2
|
-
import { and, desc, eq, inArray, sql } from 'drizzle-orm';
|
|
3
|
-
import type { Database } from '../client.js';
|
|
4
|
-
import { type Paginated, paginate } from '../pagination.js';
|
|
5
|
-
import type { Pagination } from '../query.js';
|
|
6
|
-
import {
|
|
7
|
-
accounts,
|
|
8
|
-
type MembershipRow,
|
|
9
|
-
memberships,
|
|
10
|
-
roles,
|
|
11
|
-
type SpaceRow,
|
|
12
|
-
sessions,
|
|
13
|
-
spaces,
|
|
14
|
-
type UserRow,
|
|
15
|
-
users,
|
|
16
|
-
} from '../schema/index.js';
|
|
17
|
-
|
|
18
|
-
/** The name of a role in a space: one of the built-in five, or a row in `roles`. */
|
|
19
|
-
export type SpaceRole = string;
|
|
20
|
-
|
|
21
|
-
/** What it takes to create an account that can sign in with a password. */
|
|
22
|
-
export interface UserCreateData {
|
|
23
|
-
name: string;
|
|
24
|
-
email: string;
|
|
25
|
-
role: string;
|
|
26
|
-
/** Already hashed; the repository never sees a plaintext password. */
|
|
27
|
-
passwordHash: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface UserUpdateData {
|
|
31
|
-
name?: string;
|
|
32
|
-
email?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* How better-auth 1.7 identifies an email + password credential: sign-in looks for an
|
|
37
|
-
* account with this provider *and* this issuer, so a row missing either is invisible to
|
|
38
|
-
* it and the account can never sign in.
|
|
39
|
-
*/
|
|
40
|
-
const CREDENTIAL_PROVIDER = 'credential';
|
|
41
|
-
const CREDENTIAL_ISSUER = 'local:credential';
|
|
42
|
-
|
|
43
|
-
export class UserRepository {
|
|
44
|
-
constructor(private readonly db: Database) {}
|
|
45
|
-
|
|
46
|
-
async findById(id: string): Promise<UserRow | null> {
|
|
47
|
-
const rows = await this.db.select().from(users).where(eq(users.id, id)).limit(1);
|
|
48
|
-
return rows[0] ?? null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async findManyByIds(ids: string[]): Promise<UserRow[]> {
|
|
52
|
-
if (ids.length === 0) return [];
|
|
53
|
-
return this.db.select().from(users).where(inArray(users.id, ids));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async findByEmail(email: string): Promise<UserRow | null> {
|
|
57
|
-
const rows = await this.db.select().from(users).where(eq(users.email, email)).limit(1);
|
|
58
|
-
return rows[0] ?? null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async list(pagination: Pagination, search?: string): Promise<Paginated<UserRow>> {
|
|
62
|
-
const where = search
|
|
63
|
-
? sql`${users.email} ilike ${`%${search}%`} or ${users.name} ilike ${`%${search}%`}`
|
|
64
|
-
: undefined;
|
|
65
|
-
|
|
66
|
-
return paginate(this.db, users, { where, orderBy: desc(users.createdAt), pagination });
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Users who are not members of a space, matching a search, newest first: the
|
|
71
|
-
* add-member picker's candidates, decided in SQL rather than by loading a page of
|
|
72
|
-
* users and filtering it here.
|
|
73
|
-
*/
|
|
74
|
-
async candidates(spaceId: string, search: string | undefined, limit: number): Promise<UserRow[]> {
|
|
75
|
-
const notMember = sql`not exists (select 1 from ${memberships} where ${memberships.userId} = ${users.id} and ${memberships.spaceId} = ${spaceId})`;
|
|
76
|
-
const where = search
|
|
77
|
-
? and(
|
|
78
|
-
notMember,
|
|
79
|
-
sql`(${users.email} ilike ${`%${search}%`} or ${users.name} ilike ${`%${search}%`})`,
|
|
80
|
-
)
|
|
81
|
-
: notMember;
|
|
82
|
-
return this.db.select().from(users).where(where).orderBy(desc(users.createdAt)).limit(limit);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async count(): Promise<number> {
|
|
86
|
-
const rows = await this.db.select({ count: sql<number>`count(*)::int` }).from(users);
|
|
87
|
-
return rows[0]?.count ?? 0;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Inserts the user and its password credential together, so a failure on the second
|
|
92
|
-
* row cannot leave an account nobody can sign in to. The account row is shaped the way
|
|
93
|
-
* better-auth writes it on sign-up, so a sign-in later finds it as its own.
|
|
94
|
-
*/
|
|
95
|
-
async create(data: UserCreateData): Promise<UserRow> {
|
|
96
|
-
return this.db.transaction(async (tx) => {
|
|
97
|
-
const [user] = await tx
|
|
98
|
-
.insert(users)
|
|
99
|
-
.values({ name: data.name, email: data.email, role: data.role })
|
|
100
|
-
.returning();
|
|
101
|
-
if (!user) throw new ManabloxError('user.create.failed');
|
|
102
|
-
await tx.insert(accounts).values({
|
|
103
|
-
userId: user.id,
|
|
104
|
-
accountId: user.id,
|
|
105
|
-
providerId: CREDENTIAL_PROVIDER,
|
|
106
|
-
issuer: CREDENTIAL_ISSUER,
|
|
107
|
-
password: data.passwordHash,
|
|
108
|
-
});
|
|
109
|
-
return user;
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async update(id: string, data: UserUpdateData): Promise<UserRow> {
|
|
114
|
-
const [row] = await this.db
|
|
115
|
-
.update(users)
|
|
116
|
-
.set({ ...data, updatedAt: new Date() })
|
|
117
|
-
.where(eq(users.id, id))
|
|
118
|
-
.returning();
|
|
119
|
-
if (!row) throw ManabloxError.notFound('user.notFound', { id });
|
|
120
|
-
return row;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
async delete(id: string): Promise<void> {
|
|
124
|
-
// Sessions, accounts, api keys and memberships cascade in the schema.
|
|
125
|
-
await this.db.delete(users).where(eq(users.id, id));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async setBanned(id: string, banned: boolean, reason: string | null): Promise<UserRow> {
|
|
129
|
-
const [row] = await this.db
|
|
130
|
-
.update(users)
|
|
131
|
-
.set({ banned, banReason: banned ? reason : null, updatedAt: new Date() })
|
|
132
|
-
.where(eq(users.id, id))
|
|
133
|
-
.returning();
|
|
134
|
-
if (!row) throw ManabloxError.notFound('user.notFound', { id });
|
|
135
|
-
return row;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Replaces the password credential, creating it for an account that only ever signed
|
|
140
|
-
* in through another provider.
|
|
141
|
-
*/
|
|
142
|
-
async setPasswordHash(userId: string, passwordHash: string): Promise<void> {
|
|
143
|
-
const updated = await this.db
|
|
144
|
-
.update(accounts)
|
|
145
|
-
.set({ password: passwordHash, updatedAt: new Date() })
|
|
146
|
-
.where(
|
|
147
|
-
and(
|
|
148
|
-
eq(accounts.userId, userId),
|
|
149
|
-
eq(accounts.providerId, CREDENTIAL_PROVIDER),
|
|
150
|
-
eq(accounts.issuer, CREDENTIAL_ISSUER),
|
|
151
|
-
),
|
|
152
|
-
)
|
|
153
|
-
.returning({ id: accounts.id });
|
|
154
|
-
if (updated.length) return;
|
|
155
|
-
await this.db.insert(accounts).values({
|
|
156
|
-
userId,
|
|
157
|
-
accountId: userId,
|
|
158
|
-
providerId: CREDENTIAL_PROVIDER,
|
|
159
|
-
issuer: CREDENTIAL_ISSUER,
|
|
160
|
-
password: passwordHash,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Signs the user out everywhere. */
|
|
165
|
-
async revokeSessions(userId: string): Promise<void> {
|
|
166
|
-
await this.db.delete(sessions).where(eq(sessions.userId, userId));
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async countByRole(role: string): Promise<number> {
|
|
170
|
-
const rows = await this.db
|
|
171
|
-
.select({ count: sql<number>`count(*)::int` })
|
|
172
|
-
.from(users)
|
|
173
|
-
.where(eq(users.role, role));
|
|
174
|
-
return rows[0]?.count ?? 0;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async setRole(id: string, role: string): Promise<UserRow> {
|
|
178
|
-
const [row] = await this.db
|
|
179
|
-
.update(users)
|
|
180
|
-
.set({ role, updatedAt: new Date() })
|
|
181
|
-
.where(eq(users.id, id))
|
|
182
|
-
.returning();
|
|
183
|
-
if (!row) throw ManabloxError.notFound('user.notFound', { id });
|
|
184
|
-
return row;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// --- space membership -----------------------------------------------------
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Authoritative role plus space memberships in one query.
|
|
191
|
-
*
|
|
192
|
-
* Read on every authenticated request rather than trusting the role embedded in the
|
|
193
|
-
* session: better-auth caches the session payload (five minutes by default), so a
|
|
194
|
-
* promotion or demotion would otherwise not take effect until that cache expired.
|
|
195
|
-
*
|
|
196
|
-
* A membership naming a custom role joins that role's grants; one naming a built-in
|
|
197
|
-
* role has none here, and the auth package answers those from its own table.
|
|
198
|
-
*/
|
|
199
|
-
async principal(userId: string): Promise<{
|
|
200
|
-
role: string;
|
|
201
|
-
banned: boolean;
|
|
202
|
-
spaces: Record<string, SpaceRole>;
|
|
203
|
-
permissions: Record<string, string[]>;
|
|
204
|
-
} | null> {
|
|
205
|
-
const rows = await this.db
|
|
206
|
-
.select({
|
|
207
|
-
role: users.role,
|
|
208
|
-
banned: users.banned,
|
|
209
|
-
spaceId: memberships.spaceId,
|
|
210
|
-
spaceRole: memberships.role,
|
|
211
|
-
grants: roles.permissions,
|
|
212
|
-
})
|
|
213
|
-
.from(users)
|
|
214
|
-
.leftJoin(memberships, eq(memberships.userId, users.id))
|
|
215
|
-
.leftJoin(
|
|
216
|
-
roles,
|
|
217
|
-
and(eq(roles.spaceId, memberships.spaceId), eq(roles.machineName, memberships.role)),
|
|
218
|
-
)
|
|
219
|
-
.where(eq(users.id, userId));
|
|
220
|
-
|
|
221
|
-
const first = rows[0];
|
|
222
|
-
if (!first) return null;
|
|
223
|
-
|
|
224
|
-
const spaces: Record<string, SpaceRole> = {};
|
|
225
|
-
const permissions: Record<string, string[]> = {};
|
|
226
|
-
for (const row of rows) {
|
|
227
|
-
if (!row.spaceId || !row.spaceRole) continue;
|
|
228
|
-
spaces[row.spaceId] = row.spaceRole;
|
|
229
|
-
if (row.grants) permissions[row.spaceId] = row.grants;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return { role: first.role, banned: first.banned, spaces, permissions };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async memberships(userId: string): Promise<MembershipRow[]> {
|
|
236
|
-
return this.db.select().from(memberships).where(eq(memberships.userId, userId));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** The user's memberships with the space each one is in, for a per-user view. */
|
|
240
|
-
async membershipsWithSpaces(userId: string): Promise<Array<MembershipRow & { space: SpaceRow }>> {
|
|
241
|
-
const rows = await this.db
|
|
242
|
-
.select({ membership: memberships, space: spaces })
|
|
243
|
-
.from(memberships)
|
|
244
|
-
.innerJoin(spaces, eq(spaces.id, memberships.spaceId))
|
|
245
|
-
.where(eq(memberships.userId, userId))
|
|
246
|
-
.orderBy(spaces.name);
|
|
247
|
-
return rows.map((row) => ({ ...row.membership, space: row.space }));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async membersOf(spaceId: string): Promise<Array<MembershipRow & { user: UserRow }>> {
|
|
251
|
-
const rows = await this.db
|
|
252
|
-
.select({ membership: memberships, user: users })
|
|
253
|
-
.from(memberships)
|
|
254
|
-
.innerJoin(users, eq(users.id, memberships.userId))
|
|
255
|
-
.where(eq(memberships.spaceId, spaceId));
|
|
256
|
-
return rows.map((row) => ({ ...row.membership, user: row.user }));
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async roleIn(userId: string, spaceId: string): Promise<SpaceRole | null> {
|
|
260
|
-
const rows = await this.db
|
|
261
|
-
.select({ role: memberships.role })
|
|
262
|
-
.from(memberships)
|
|
263
|
-
.where(and(eq(memberships.userId, userId), eq(memberships.spaceId, spaceId)))
|
|
264
|
-
.limit(1);
|
|
265
|
-
return rows[0]?.role ?? null;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async grant(userId: string, spaceId: string, role: SpaceRole): Promise<void> {
|
|
269
|
-
await this.db
|
|
270
|
-
.insert(memberships)
|
|
271
|
-
.values({ userId, spaceId, role })
|
|
272
|
-
.onConflictDoUpdate({ target: [memberships.userId, memberships.spaceId], set: { role } });
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async revoke(userId: string, spaceId: string): Promise<void> {
|
|
276
|
-
await this.db
|
|
277
|
-
.delete(memberships)
|
|
278
|
-
.where(and(eq(memberships.userId, userId), eq(memberships.spaceId, spaceId)));
|
|
279
|
-
}
|
|
280
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { and, eq } from 'drizzle-orm';
|
|
2
|
-
import type { Database } from '../client.js';
|
|
3
|
-
import { webhookDeliveries, webhooks } from '../schema/index.js';
|
|
4
|
-
|
|
5
|
-
export type WebhookRow = typeof webhooks.$inferSelect;
|
|
6
|
-
export type WebhookDeliveryRow = typeof webhookDeliveries.$inferSelect;
|
|
7
|
-
|
|
8
|
-
export interface WebhookDeliveryData {
|
|
9
|
-
webhookId: string;
|
|
10
|
-
event: string;
|
|
11
|
-
payload: Record<string, unknown>;
|
|
12
|
-
status: number | null;
|
|
13
|
-
error: string | null;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** The webhooks of a space and the log of what was sent to them. */
|
|
17
|
-
export class WebhookRepository {
|
|
18
|
-
constructor(private readonly db: Database) {}
|
|
19
|
-
|
|
20
|
-
async findById(id: string): Promise<WebhookRow | null> {
|
|
21
|
-
const rows = await this.db.select().from(webhooks).where(eq(webhooks.id, id)).limit(1);
|
|
22
|
-
return rows[0] ?? null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** The switched-on webhooks of a space, for fanning an event out. */
|
|
26
|
-
async findEnabled(spaceId: string): Promise<WebhookRow[]> {
|
|
27
|
-
return this.db
|
|
28
|
-
.select()
|
|
29
|
-
.from(webhooks)
|
|
30
|
-
.where(and(eq(webhooks.spaceId, spaceId), eq(webhooks.enabled, true)));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async recordDelivery(data: WebhookDeliveryData): Promise<WebhookDeliveryRow> {
|
|
34
|
-
const [row] = await this.db.insert(webhookDeliveries).values(data).returning();
|
|
35
|
-
return row as WebhookDeliveryRow;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async deliveries(webhookId: string, limit = 50): Promise<WebhookDeliveryRow[]> {
|
|
39
|
-
return this.db
|
|
40
|
-
.select()
|
|
41
|
-
.from(webhookDeliveries)
|
|
42
|
-
.where(eq(webhookDeliveries.webhookId, webhookId))
|
|
43
|
-
.orderBy(webhookDeliveries.createdAt)
|
|
44
|
-
.limit(limit);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Loose,
|
|
3
|
-
ManabloxError,
|
|
4
|
-
type WorkflowCursor,
|
|
5
|
-
type WorkflowRunContext,
|
|
6
|
-
type WorkflowRunStatus,
|
|
7
|
-
type WorkflowSelection,
|
|
8
|
-
type WorkflowStep,
|
|
9
|
-
type WorkflowStepLog,
|
|
10
|
-
type WorkflowTrigger,
|
|
11
|
-
} from '@manablox/core';
|
|
12
|
-
import { and, desc, eq, gte, inArray, isNull, lt, lte, or, sql } from 'drizzle-orm';
|
|
13
|
-
import type { Database } from '../client.js';
|
|
14
|
-
import {
|
|
15
|
-
type ContentRow,
|
|
16
|
-
contents,
|
|
17
|
-
type PushSubscriptionRow,
|
|
18
|
-
pushSubscriptions,
|
|
19
|
-
type WorkflowRow,
|
|
20
|
-
type WorkflowRunRow,
|
|
21
|
-
workflowRuns,
|
|
22
|
-
workflows,
|
|
23
|
-
} from '../schema/index.js';
|
|
24
|
-
|
|
25
|
-
export interface WorkflowWriteData {
|
|
26
|
-
id?: string | undefined;
|
|
27
|
-
spaceId: string;
|
|
28
|
-
name: string;
|
|
29
|
-
description?: string | null | undefined;
|
|
30
|
-
enabled?: boolean | undefined;
|
|
31
|
-
trigger: WorkflowTrigger;
|
|
32
|
-
steps: WorkflowStep[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface WorkflowRunCreateData {
|
|
36
|
-
workflowId: string;
|
|
37
|
-
spaceId: string;
|
|
38
|
-
trigger: string;
|
|
39
|
-
context: WorkflowRunContext;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** How many runs a workflow keeps; older ones are pruned as new ones are written. */
|
|
43
|
-
export const RUNS_KEPT_PER_WORKFLOW = 200;
|
|
44
|
-
|
|
45
|
-
export class WorkflowRepository {
|
|
46
|
-
constructor(private readonly db: Database) {}
|
|
47
|
-
|
|
48
|
-
// --- workflows -------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
async listBySpace(spaceId: string): Promise<WorkflowRow[]> {
|
|
51
|
-
return this.db
|
|
52
|
-
.select()
|
|
53
|
-
.from(workflows)
|
|
54
|
-
.where(eq(workflows.spaceId, spaceId))
|
|
55
|
-
.orderBy(workflows.name);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Every enabled workflow of a space, for the dispatcher; every enabled one at all for the scheduler. */
|
|
59
|
-
async listEnabled(spaceId?: string): Promise<WorkflowRow[]> {
|
|
60
|
-
return this.db
|
|
61
|
-
.select()
|
|
62
|
-
.from(workflows)
|
|
63
|
-
.where(
|
|
64
|
-
spaceId
|
|
65
|
-
? and(eq(workflows.enabled, true), eq(workflows.spaceId, spaceId))
|
|
66
|
-
: eq(workflows.enabled, true),
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async findById(id: string): Promise<WorkflowRow | null> {
|
|
71
|
-
const rows = await this.db.select().from(workflows).where(eq(workflows.id, id)).limit(1);
|
|
72
|
-
return rows[0] ?? null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async create(data: WorkflowWriteData): Promise<WorkflowRow> {
|
|
76
|
-
const [row] = await this.db
|
|
77
|
-
.insert(workflows)
|
|
78
|
-
.values({
|
|
79
|
-
...(data.id ? { id: data.id } : {}),
|
|
80
|
-
spaceId: data.spaceId,
|
|
81
|
-
name: data.name,
|
|
82
|
-
description: data.description ?? null,
|
|
83
|
-
enabled: data.enabled ?? false,
|
|
84
|
-
trigger: data.trigger,
|
|
85
|
-
steps: data.steps,
|
|
86
|
-
})
|
|
87
|
-
.returning();
|
|
88
|
-
if (!row) throw new ManabloxError('workflow.create.failed');
|
|
89
|
-
return row;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async update(id: string, data: Loose<Omit<WorkflowWriteData, 'spaceId'>>): Promise<WorkflowRow> {
|
|
93
|
-
const [row] = await this.db
|
|
94
|
-
.update(workflows)
|
|
95
|
-
.set({
|
|
96
|
-
...(data.name !== undefined ? { name: data.name } : {}),
|
|
97
|
-
...(data.description !== undefined ? { description: data.description } : {}),
|
|
98
|
-
...(data.enabled !== undefined ? { enabled: data.enabled } : {}),
|
|
99
|
-
...(data.trigger !== undefined ? { trigger: data.trigger } : {}),
|
|
100
|
-
...(data.steps !== undefined ? { steps: data.steps } : {}),
|
|
101
|
-
updatedAt: new Date(),
|
|
102
|
-
})
|
|
103
|
-
.where(eq(workflows.id, id))
|
|
104
|
-
.returning();
|
|
105
|
-
if (!row) throw ManabloxError.notFound('workflow.notFound', { id });
|
|
106
|
-
return row;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async delete(id: string): Promise<void> {
|
|
110
|
-
await this.db.delete(workflows).where(eq(workflows.id, id));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Claims a scheduled workflow for one minute. Returns false when another process got
|
|
115
|
-
* there first — the update matches nothing once `lastScheduledAt` is already `minute`.
|
|
116
|
-
*/
|
|
117
|
-
async claimSchedule(id: string, minute: Date): Promise<boolean> {
|
|
118
|
-
const rows = await this.db
|
|
119
|
-
.update(workflows)
|
|
120
|
-
.set({ lastScheduledAt: minute })
|
|
121
|
-
.where(
|
|
122
|
-
and(
|
|
123
|
-
eq(workflows.id, id),
|
|
124
|
-
or(isNull(workflows.lastScheduledAt), lt(workflows.lastScheduledAt, minute)),
|
|
125
|
-
),
|
|
126
|
-
)
|
|
127
|
-
.returning({ id: workflows.id });
|
|
128
|
-
return rows.length > 0;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async touchRun(id: string, at: Date): Promise<void> {
|
|
132
|
-
await this.db.update(workflows).set({ lastRunAt: at }).where(eq(workflows.id, id));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// --- runs ------------------------------------------------------------------
|
|
136
|
-
|
|
137
|
-
async createRun(data: WorkflowRunCreateData): Promise<WorkflowRunRow> {
|
|
138
|
-
const [row] = await this.db
|
|
139
|
-
.insert(workflowRuns)
|
|
140
|
-
.values({
|
|
141
|
-
workflowId: data.workflowId,
|
|
142
|
-
spaceId: data.spaceId,
|
|
143
|
-
trigger: data.trigger,
|
|
144
|
-
context: data.context,
|
|
145
|
-
status: 'queued',
|
|
146
|
-
})
|
|
147
|
-
.returning();
|
|
148
|
-
if (!row) throw new ManabloxError('workflow.create.failed');
|
|
149
|
-
await this.pruneRuns(data.workflowId);
|
|
150
|
-
return row;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async findRun(id: string): Promise<WorkflowRunRow | null> {
|
|
154
|
-
const rows = await this.db.select().from(workflowRuns).where(eq(workflowRuns.id, id)).limit(1);
|
|
155
|
-
return rows[0] ?? null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async listRuns(workflowId: string, limit = 50): Promise<WorkflowRunRow[]> {
|
|
159
|
-
return this.db
|
|
160
|
-
.select()
|
|
161
|
-
.from(workflowRuns)
|
|
162
|
-
.where(eq(workflowRuns.workflowId, workflowId))
|
|
163
|
-
.orderBy(desc(workflowRuns.createdAt))
|
|
164
|
-
.limit(limit);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Moves a run from `queued` or `waiting` to `running`, or reports that it is not
|
|
169
|
-
* there to be moved. The status check in the predicate is what keeps two workers off
|
|
170
|
-
* the same run.
|
|
171
|
-
*/
|
|
172
|
-
async claimRun(id: string): Promise<WorkflowRunRow | null> {
|
|
173
|
-
const rows = await this.db
|
|
174
|
-
.update(workflowRuns)
|
|
175
|
-
.set({ status: 'running', startedAt: sql`coalesce(${workflowRuns.startedAt}, now())` })
|
|
176
|
-
.where(and(eq(workflowRuns.id, id), inArray(workflowRuns.status, ['queued', 'waiting'])))
|
|
177
|
-
.returning();
|
|
178
|
-
return rows[0] ?? null;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** Runs paused by a delay step whose time has come. */
|
|
182
|
-
async dueRuns(now: Date, limit = 100): Promise<WorkflowRunRow[]> {
|
|
183
|
-
return this.db
|
|
184
|
-
.select()
|
|
185
|
-
.from(workflowRuns)
|
|
186
|
-
.where(and(eq(workflowRuns.status, 'waiting'), lte(workflowRuns.resumeAt, now)))
|
|
187
|
-
.orderBy(workflowRuns.resumeAt)
|
|
188
|
-
.limit(limit);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async saveRunProgress(
|
|
192
|
-
id: string,
|
|
193
|
-
data: {
|
|
194
|
-
status: WorkflowRunStatus;
|
|
195
|
-
cursor: WorkflowCursor;
|
|
196
|
-
log: WorkflowStepLog[];
|
|
197
|
-
error?: string | null | undefined;
|
|
198
|
-
resumeAt?: Date | null | undefined;
|
|
199
|
-
finished?: boolean | undefined;
|
|
200
|
-
},
|
|
201
|
-
): Promise<void> {
|
|
202
|
-
await this.db
|
|
203
|
-
.update(workflowRuns)
|
|
204
|
-
.set({
|
|
205
|
-
status: data.status,
|
|
206
|
-
cursor: data.cursor,
|
|
207
|
-
log: data.log,
|
|
208
|
-
error: data.error ?? null,
|
|
209
|
-
resumeAt: data.resumeAt ?? null,
|
|
210
|
-
...(data.finished ? { finishedAt: new Date() } : {}),
|
|
211
|
-
})
|
|
212
|
-
.where(eq(workflowRuns.id, id));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
private async pruneRuns(workflowId: string): Promise<void> {
|
|
216
|
-
await this.db.execute(sql`
|
|
217
|
-
delete from ${workflowRuns}
|
|
218
|
-
where ${workflowRuns.workflowId} = ${workflowId}
|
|
219
|
-
and ${workflowRuns.id} in (
|
|
220
|
-
select id from ${workflowRuns}
|
|
221
|
-
where ${workflowRuns.workflowId} = ${workflowId}
|
|
222
|
-
order by ${workflowRuns.createdAt} desc
|
|
223
|
-
offset ${RUNS_KEPT_PER_WORKFLOW}
|
|
224
|
-
)
|
|
225
|
-
`);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// --- documents for scheduled runs -----------------------------------------
|
|
229
|
-
|
|
230
|
-
/** The documents a scheduled workflow's selection names, newest change first. */
|
|
231
|
-
async selectDocuments(
|
|
232
|
-
spaceId: string,
|
|
233
|
-
selection: WorkflowSelection,
|
|
234
|
-
limit = 500,
|
|
235
|
-
): Promise<ContentRow[]> {
|
|
236
|
-
const predicates = [eq(contents.spaceId, spaceId)];
|
|
237
|
-
if (selection.typeIds.length) predicates.push(inArray(contents.typeId, selection.typeIds));
|
|
238
|
-
if (selection.status !== 'any') predicates.push(eq(contents.status, selection.status));
|
|
239
|
-
if (selection.locale) predicates.push(eq(contents.locale, selection.locale));
|
|
240
|
-
if (selection.changedWithinHours) {
|
|
241
|
-
const since = new Date(Date.now() - selection.changedWithinHours * 3_600_000);
|
|
242
|
-
predicates.push(gte(contents.updatedAt, since));
|
|
243
|
-
}
|
|
244
|
-
return this.db
|
|
245
|
-
.select()
|
|
246
|
-
.from(contents)
|
|
247
|
-
.where(and(...predicates))
|
|
248
|
-
.orderBy(desc(contents.updatedAt))
|
|
249
|
-
.limit(limit);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// --- push subscriptions ----------------------------------------------------
|
|
253
|
-
|
|
254
|
-
async subscriptionsFor(userIds: string[]): Promise<PushSubscriptionRow[]> {
|
|
255
|
-
if (userIds.length === 0) return [];
|
|
256
|
-
return this.db
|
|
257
|
-
.select()
|
|
258
|
-
.from(pushSubscriptions)
|
|
259
|
-
.where(inArray(pushSubscriptions.userId, userIds));
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async subscriptionsOf(userId: string): Promise<PushSubscriptionRow[]> {
|
|
263
|
-
return this.db
|
|
264
|
-
.select()
|
|
265
|
-
.from(pushSubscriptions)
|
|
266
|
-
.where(eq(pushSubscriptions.userId, userId))
|
|
267
|
-
.orderBy(desc(pushSubscriptions.createdAt));
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/** Upserts on the endpoint: a browser re-subscribing keeps one row, not two. */
|
|
271
|
-
async subscribe(data: {
|
|
272
|
-
userId: string;
|
|
273
|
-
endpoint: string;
|
|
274
|
-
keys: { p256dh: string; auth: string };
|
|
275
|
-
userAgent: string | null;
|
|
276
|
-
}): Promise<PushSubscriptionRow> {
|
|
277
|
-
const [row] = await this.db
|
|
278
|
-
.insert(pushSubscriptions)
|
|
279
|
-
.values(data)
|
|
280
|
-
.onConflictDoUpdate({
|
|
281
|
-
target: pushSubscriptions.endpoint,
|
|
282
|
-
set: { userId: data.userId, keys: data.keys, userAgent: data.userAgent },
|
|
283
|
-
})
|
|
284
|
-
.returning();
|
|
285
|
-
if (!row) throw new ManabloxError('workflow.create.failed');
|
|
286
|
-
return row;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async unsubscribe(userId: string, endpoint: string): Promise<void> {
|
|
290
|
-
await this.db
|
|
291
|
-
.delete(pushSubscriptions)
|
|
292
|
-
.where(and(eq(pushSubscriptions.userId, userId), eq(pushSubscriptions.endpoint, endpoint)));
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** A push service answered 404/410: the browser is gone, and so is the row. */
|
|
296
|
-
async dropSubscription(id: string): Promise<void> {
|
|
297
|
-
await this.db.delete(pushSubscriptions).where(eq(pushSubscriptions.id, id));
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async markSubscriptionUsed(id: string): Promise<void> {
|
|
301
|
-
await this.db
|
|
302
|
-
.update(pushSubscriptions)
|
|
303
|
-
.set({ lastUsedAt: new Date() })
|
|
304
|
-
.where(eq(pushSubscriptions.id, id));
|
|
305
|
-
}
|
|
306
|
-
}
|