@tatara/cli 0.7.0 → 0.9.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tatara/cli",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Create and grow opinionated Hono backends",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,15 @@
1
+ import { createAuthz } from "@tatara/authz";
2
+ import { drizzleAuthzStore } from "@tatara/authz/drizzle";
3
+ import * as tables from "#core/authz/db/authz.schema";
4
+ import { SYSTEM_ROLES } from "#core/authz/system-roles";
5
+ import { db } from "#core/db/client";
6
+ import { catalog, type PermissionKey } from "#generated/permissions";
7
+
8
+ /** The app's authorization: its tables (authz.schema.ts), the catalog from `tatara sync`, built-in roles. */
9
+ export const authz = createAuthz<PermissionKey>({
10
+ store: drizzleAuthzStore(db, tables),
11
+ catalog,
12
+ systemRoles: SYSTEM_ROLES,
13
+ });
14
+
15
+ export const { checkPermission, removeMemberAccess, requirePermission, resolveScope } = authz;
@@ -0,0 +1,14 @@
1
+ import { createAuthz } from "@tatara/authz";
2
+ import { prismaAuthzStore } from "@tatara/authz/prisma";
3
+ import { SYSTEM_ROLES } from "#core/authz/system-roles";
4
+ import { db } from "#core/db/client";
5
+ import { catalog, type PermissionKey } from "#generated/permissions";
6
+
7
+ /** The app's authorization: its tables (prisma/models/authz.prisma), the catalog from `tatara sync`, built-in roles. */
8
+ export const authz = createAuthz<PermissionKey>({
9
+ store: prismaAuthzStore(db),
10
+ catalog,
11
+ systemRoles: SYSTEM_ROLES,
12
+ });
13
+
14
+ export const { checkPermission, removeMemberAccess, requirePermission, resolveScope } = authz;
@@ -31,15 +31,15 @@
31
31
  "@aws-sdk/s3-request-presigner": "3.1152.0",
32
32
  "nodemailer": "10.1.1",
33
33
  "@types/nodemailer": "8.0.2",
34
- "@tatara/core": "0.1.0",
35
- "@tatara/http": "0.1.0",
36
- "@tatara/db": "0.1.0",
37
- "@tatara/testing": "0.1.0",
38
- "@tatara/auth": "0.1.0",
39
- "@tatara/keycloak": "0.1.0",
40
- "@tatara/authz": "0.1.0",
41
- "@tatara/storage": "0.1.0",
42
- "@tatara/email": "0.1.0",
34
+ "@tatara/core": "0.1.1",
35
+ "@tatara/http": "0.1.1",
36
+ "@tatara/db": "0.2.0",
37
+ "@tatara/testing": "0.1.1",
38
+ "@tatara/auth": "0.1.1",
39
+ "@tatara/keycloak": "0.1.1",
40
+ "@tatara/authz": "0.2.0",
41
+ "@tatara/storage": "0.2.0",
42
+ "@tatara/email": "0.2.0",
43
43
  "@tatara/i18n": "0.1.0",
44
- "@tatara/jobs": "0.1.0"
44
+ "@tatara/jobs": "0.2.0"
45
45
  }
@@ -1,21 +0,0 @@
1
- import { z } from "zod";
2
- import { DEFAULT_ORG_ID, SYSTEM_ORG_ID } from "@tatara/authz";
3
- import { authz } from "#core/authz/index";
4
- import { defineCommand } from "@tatara/core";
5
-
6
- export default defineCommand({
7
- name: "authz:grant-role",
8
- description: "Assign a role (built-in key or custom role key) to a user in an org",
9
- args: z.object({
10
- user: z.string().min(1),
11
- role: z.string().min(1),
12
- org: z.string().default(DEFAULT_ORG_ID),
13
- }),
14
- async run({ user, role, org }, { logger }) {
15
- const found = (await authz.store.findRoleByKey(org, role)) ??
16
- (await authz.store.findRoleByKey(SYSTEM_ORG_ID, role));
17
- if (!found) throw new Error(`No role "${role}" (run authz:sync-permissions first for built-in roles)`);
18
- const added = await authz.assignRole(null, org, user, found.id);
19
- logger.info({ user, role, org, added }, added ? "role assigned" : "role already assigned");
20
- },
21
- });
@@ -1,11 +0,0 @@
1
- import { defineCommand } from "@tatara/core";
2
- import { authz } from "#core/authz/index";
3
-
4
- export default defineCommand({
5
- name: "authz:sync-permissions",
6
- description: "Create/update built-in roles so their grants match the code (run on every deploy)",
7
- async run(_args, { logger }) {
8
- const result = await authz.syncSystemRoles();
9
- logger.info(result, "built-in roles in sync");
10
- },
11
- });
@@ -1,10 +0,0 @@
1
- import { authz } from "#core/authz/index";
2
- import { defineSeed } from "@tatara/db";
3
-
4
- export default defineSeed({
5
- name: "authz:system-roles",
6
- order: -10,
7
- async run({ logger }) {
8
- logger.info(await authz.syncSystemRoles(), "built-in roles in sync");
9
- },
10
- });
@@ -1,9 +0,0 @@
1
- import { createAuthz } from "@tatara/authz";
2
- import { store } from "#core/authz/db/store";
3
- import { SYSTEM_ROLES } from "#core/authz/system-roles";
4
- import { catalog, type PermissionKey } from "#generated/permissions";
5
-
6
- /** The app's authorization: its store, the permission catalog from `tatara sync`, built-in roles. */
7
- export const authz = createAuthz<PermissionKey>({ store, catalog, systemRoles: SYSTEM_ROLES });
8
-
9
- export const { checkPermission, removeMemberAccess, requirePermission, resolveScope } = authz;
@@ -1,275 +0,0 @@
1
- import { and, count, desc, eq, gt, inArray, isNull, or } from "drizzle-orm";
2
- import type { OverlayEffect, Scope } from "@tatara/authz";
3
- import { SYSTEM_ORG_ID } from "@tatara/authz";
4
- import {
5
- authzAuditLog,
6
- authzMemberRoles,
7
- authzOverlays,
8
- authzRolePermissions,
9
- authzRoles,
10
- } from "#core/authz/db/authz.schema";
11
- import type { AuthzStore, OverlayRow, PermissionGrant, RoleRow } from "@tatara/authz";
12
- import { db } from "#core/db/client";
13
- import { newId } from "@tatara/db";
14
-
15
- const toScope = (scope: string): Scope | null => (scope === "" ? null : (scope as Scope));
16
-
17
- type RoleRecord = typeof authzRoles.$inferSelect;
18
- type OverlayRecord = typeof authzOverlays.$inferSelect;
19
-
20
- function toOverlay(row: OverlayRecord): OverlayRow {
21
- return { ...row, scope: toScope(row.scope), effect: row.effect as OverlayEffect };
22
- }
23
-
24
- async function hydrate(roles: RoleRecord[], orgId: string): Promise<RoleRow[]> {
25
- if (roles.length === 0) return [];
26
- const ids = roles.map((r) => r.id);
27
- const [grants, members] = await Promise.all([
28
- db.select().from(authzRolePermissions).where(inArray(authzRolePermissions.roleId, ids)),
29
- db
30
- .select({ roleId: authzMemberRoles.roleId, total: count() })
31
- .from(authzMemberRoles)
32
- .where(and(eq(authzMemberRoles.orgId, orgId), inArray(authzMemberRoles.roleId, ids)))
33
- .groupBy(authzMemberRoles.roleId),
34
- ]);
35
- return roles.map((role) => ({
36
- id: role.id,
37
- orgId: role.orgId,
38
- key: role.key,
39
- name: role.name,
40
- description: role.description,
41
- isSystem: role.isSystem,
42
- grants: grants
43
- .filter((g) => g.roleId === role.id)
44
- .map((g) => ({ resource: g.resource, action: g.action, scope: toScope(g.scope) })),
45
- memberCount: members.find((m) => m.roleId === role.id)?.total ?? 0,
46
- }));
47
- }
48
-
49
- const visibleTo = (orgId: string) => or(eq(authzRoles.orgId, SYSTEM_ORG_ID), eq(authzRoles.orgId, orgId));
50
-
51
- async function insertGrants(roleId: string, grants: PermissionGrant[]) {
52
- if (grants.length === 0) return;
53
- await db.insert(authzRolePermissions).values(
54
- grants.map((g) => ({
55
- id: newId(),
56
- roleId,
57
- resource: g.resource,
58
- action: g.action,
59
- scope: g.scope ?? "",
60
- })),
61
- );
62
- }
63
-
64
- export const store: AuthzStore = {
65
- async roleGrants(orgId, userId, impliedRoleKeys) {
66
- const assigned = await db
67
- .select({ roleId: authzMemberRoles.roleId })
68
- .from(authzMemberRoles)
69
- .where(and(eq(authzMemberRoles.orgId, orgId), eq(authzMemberRoles.userId, userId)));
70
- const implied = impliedRoleKeys.length
71
- ? await db
72
- .select({ id: authzRoles.id })
73
- .from(authzRoles)
74
- .where(and(eq(authzRoles.orgId, SYSTEM_ORG_ID), inArray(authzRoles.key, impliedRoleKeys)))
75
- : [];
76
- const roleIds = [...new Set([...assigned.map((r) => r.roleId), ...implied.map((r) => r.id)])];
77
- if (roleIds.length === 0) return { roles: [], grants: [] };
78
- const [roles, grants] = await Promise.all([
79
- db.select({ key: authzRoles.key }).from(authzRoles).where(inArray(authzRoles.id, roleIds)),
80
- db.select().from(authzRolePermissions).where(inArray(authzRolePermissions.roleId, roleIds)),
81
- ]);
82
- return {
83
- roles: roles.map((r) => r.key),
84
- grants: grants.map((g) => ({ resource: g.resource, action: g.action, scope: toScope(g.scope) })),
85
- };
86
- },
87
-
88
- async activeOverlays(orgId, userId, now) {
89
- const rows = await db
90
- .select()
91
- .from(authzOverlays)
92
- .where(
93
- and(
94
- eq(authzOverlays.orgId, orgId),
95
- eq(authzOverlays.userId, userId),
96
- or(isNull(authzOverlays.expiresAt), gt(authzOverlays.expiresAt, now)),
97
- ),
98
- );
99
- return rows.map(toOverlay);
100
- },
101
-
102
- async listRoles(orgId) {
103
- const roles = await db.select().from(authzRoles).where(visibleTo(orgId)).orderBy(authzRoles.key);
104
- return hydrate(roles, orgId);
105
- },
106
-
107
- async findRole(orgId, roleId) {
108
- const roles = await db
109
- .select()
110
- .from(authzRoles)
111
- .where(and(eq(authzRoles.id, roleId), visibleTo(orgId)));
112
- return (await hydrate(roles, orgId))[0] ?? null;
113
- },
114
-
115
- async findRoleByKey(orgId, key) {
116
- const roles = await db
117
- .select()
118
- .from(authzRoles)
119
- .where(and(eq(authzRoles.key, key), eq(authzRoles.orgId, orgId)));
120
- return (await hydrate(roles, orgId))[0] ?? null;
121
- },
122
-
123
- async createRole(input) {
124
- const id = newId();
125
- await db.insert(authzRoles).values({
126
- id,
127
- orgId: input.orgId,
128
- key: input.key,
129
- name: input.name,
130
- description: input.description,
131
- isSystem: input.isSystem,
132
- });
133
- await insertGrants(id, input.grants);
134
- return id;
135
- },
136
-
137
- async updateRole(roleId, input) {
138
- const fields = {
139
- ...(input.name === undefined ? {} : { name: input.name }),
140
- ...(input.description === undefined ? {} : { description: input.description }),
141
- };
142
- await db
143
- .update(authzRoles)
144
- .set({ ...fields, updatedAt: new Date() })
145
- .where(eq(authzRoles.id, roleId));
146
- if (input.grants) {
147
- await db.delete(authzRolePermissions).where(eq(authzRolePermissions.roleId, roleId));
148
- await insertGrants(roleId, input.grants);
149
- }
150
- },
151
-
152
- async deleteRole(roleId) {
153
- await db.delete(authzRoles).where(eq(authzRoles.id, roleId));
154
- },
155
-
156
- async memberRoles(orgId, userId) {
157
- const assigned = await db
158
- .select({ roleId: authzMemberRoles.roleId })
159
- .from(authzMemberRoles)
160
- .where(and(eq(authzMemberRoles.orgId, orgId), eq(authzMemberRoles.userId, userId)));
161
- if (assigned.length === 0) return [];
162
- const roles = await db
163
- .select()
164
- .from(authzRoles)
165
- .where(inArray(authzRoles.id, assigned.map((a) => a.roleId)));
166
- return hydrate(roles, orgId);
167
- },
168
-
169
- async assignRole(orgId, userId, roleId, by) {
170
- const existing = await db
171
- .select({ id: authzMemberRoles.id })
172
- .from(authzMemberRoles)
173
- .where(
174
- and(
175
- eq(authzMemberRoles.orgId, orgId),
176
- eq(authzMemberRoles.userId, userId),
177
- eq(authzMemberRoles.roleId, roleId),
178
- ),
179
- );
180
- if (existing.length > 0) return false;
181
- await db.insert(authzMemberRoles).values({ id: newId(), orgId, userId, roleId, assignedBy: by });
182
- return true;
183
- },
184
-
185
- async revokeRole(orgId, userId, roleId) {
186
- const where = and(
187
- eq(authzMemberRoles.orgId, orgId),
188
- eq(authzMemberRoles.userId, userId),
189
- eq(authzMemberRoles.roleId, roleId),
190
- );
191
- const existing = await db.select({ id: authzMemberRoles.id }).from(authzMemberRoles).where(where);
192
- if (existing.length === 0) return false;
193
- await db.delete(authzMemberRoles).where(where);
194
- return true;
195
- },
196
-
197
- async removeMember(orgId, userId) {
198
- const roleWhere = and(eq(authzMemberRoles.orgId, orgId), eq(authzMemberRoles.userId, userId));
199
- const overlayWhere = and(eq(authzOverlays.orgId, orgId), eq(authzOverlays.userId, userId));
200
- // count first: MySQL has no RETURNING
201
- return db.transaction(async (tx) => {
202
- const [roles] = await tx.select({ n: count() }).from(authzMemberRoles).where(roleWhere);
203
- const [overlays] = await tx.select({ n: count() }).from(authzOverlays).where(overlayWhere);
204
- await tx.delete(authzMemberRoles).where(roleWhere);
205
- await tx.delete(authzOverlays).where(overlayWhere);
206
- return { roles: roles?.n ?? 0, overlays: overlays?.n ?? 0 };
207
- });
208
- },
209
-
210
- async listOverlays(orgId, userId) {
211
- const rows = await db
212
- .select()
213
- .from(authzOverlays)
214
- .where(and(eq(authzOverlays.orgId, orgId), eq(authzOverlays.userId, userId)))
215
- .orderBy(desc(authzOverlays.createdAt));
216
- return rows.map(toOverlay);
217
- },
218
-
219
- async addOverlay(input) {
220
- const id = newId();
221
- const scope = input.scope ?? "";
222
- await db
223
- .delete(authzOverlays)
224
- .where(
225
- and(
226
- eq(authzOverlays.orgId, input.orgId),
227
- eq(authzOverlays.userId, input.userId),
228
- eq(authzOverlays.resource, input.resource),
229
- eq(authzOverlays.action, input.action),
230
- eq(authzOverlays.scope, scope),
231
- eq(authzOverlays.effect, input.effect),
232
- ),
233
- );
234
- await db.insert(authzOverlays).values({ ...input, id, scope });
235
- return id;
236
- },
237
-
238
- async removeOverlay(orgId, userId, overlayId) {
239
- const where = and(
240
- eq(authzOverlays.orgId, orgId),
241
- eq(authzOverlays.userId, userId),
242
- eq(authzOverlays.id, overlayId),
243
- );
244
- const [row] = await db.select().from(authzOverlays).where(where);
245
- if (!row) return null;
246
- await db.delete(authzOverlays).where(where);
247
- return toOverlay(row);
248
- },
249
-
250
- async writeAudit(input) {
251
- await db.insert(authzAuditLog).values({
252
- id: newId(),
253
- orgId: input.orgId,
254
- actorId: input.actorId,
255
- targetUserId: input.targetUserId,
256
- changeType: input.changeType,
257
- before: input.before === undefined ? null : JSON.stringify(input.before),
258
- after: input.after === undefined ? null : JSON.stringify(input.after),
259
- });
260
- },
261
-
262
- async listAudit(orgId, limit) {
263
- const rows = await db
264
- .select()
265
- .from(authzAuditLog)
266
- .where(eq(authzAuditLog.orgId, orgId))
267
- .orderBy(desc(authzAuditLog.id))
268
- .limit(limit);
269
- return rows.map((r) => ({
270
- ...r,
271
- before: r.before ? (JSON.parse(r.before) as unknown) : null,
272
- after: r.after ? (JSON.parse(r.after) as unknown) : null,
273
- }));
274
- },
275
- };
@@ -1,236 +0,0 @@
1
- import type { OverlayEffect, Scope } from "@tatara/authz";
2
- import { SYSTEM_ORG_ID } from "@tatara/authz";
3
- import type { AuthzStore, OverlayRow, PermissionGrant, RoleRow } from "@tatara/authz";
4
- import { db } from "#core/db/client";
5
- import { newId } from "@tatara/db";
6
-
7
- const toScope = (scope: string): Scope | null => (scope === "" ? null : (scope as Scope));
8
-
9
- interface OverlayRecord {
10
- id: string;
11
- orgId: string;
12
- userId: string;
13
- resource: string;
14
- action: string;
15
- scope: string;
16
- effect: string;
17
- grantedBy: string | null;
18
- reason: string | null;
19
- expiresAt: Date | null;
20
- createdAt: Date;
21
- }
22
-
23
- function toOverlay(row: OverlayRecord): OverlayRow {
24
- return { ...row, scope: toScope(row.scope), effect: row.effect as OverlayEffect };
25
- }
26
-
27
- const roleInclude = (orgId: string) => ({
28
- permissions: true,
29
- _count: { select: { members: { where: { orgId } } } },
30
- });
31
-
32
- interface RoleRecord {
33
- id: string;
34
- orgId: string;
35
- key: string;
36
- name: string;
37
- description: string | null;
38
- isSystem: boolean;
39
- permissions: { resource: string; action: string; scope: string }[];
40
- _count: { members: number };
41
- }
42
-
43
- function toRole(role: RoleRecord): RoleRow {
44
- return {
45
- id: role.id,
46
- orgId: role.orgId,
47
- key: role.key,
48
- name: role.name,
49
- description: role.description,
50
- isSystem: role.isSystem,
51
- grants: role.permissions.map((g) => ({
52
- resource: g.resource,
53
- action: g.action,
54
- scope: toScope(g.scope),
55
- })),
56
- memberCount: role._count.members,
57
- };
58
- }
59
-
60
- const grantRows = (grants: PermissionGrant[]) =>
61
- grants.map((g) => ({ id: newId(), resource: g.resource, action: g.action, scope: g.scope ?? "" }));
62
-
63
- export const store: AuthzStore = {
64
- async roleGrants(orgId, userId, impliedRoleKeys) {
65
- const roles = await db.authzRole.findMany({
66
- where: {
67
- OR: [
68
- { members: { some: { orgId, userId } } },
69
- ...(impliedRoleKeys.length
70
- ? [{ orgId: SYSTEM_ORG_ID, key: { in: impliedRoleKeys } }]
71
- : []),
72
- ],
73
- },
74
- include: { permissions: true },
75
- });
76
- return {
77
- roles: roles.map((r) => r.key),
78
- grants: roles.flatMap((r) =>
79
- r.permissions.map((g) => ({ resource: g.resource, action: g.action, scope: toScope(g.scope) })),
80
- ),
81
- };
82
- },
83
-
84
- async activeOverlays(orgId, userId, now) {
85
- const rows = await db.authzOverlay.findMany({
86
- where: { orgId, userId, OR: [{ expiresAt: null }, { expiresAt: { gt: now } }] },
87
- });
88
- return rows.map(toOverlay);
89
- },
90
-
91
- async listRoles(orgId) {
92
- const roles = await db.authzRole.findMany({
93
- where: { orgId: { in: [SYSTEM_ORG_ID, orgId] } },
94
- include: roleInclude(orgId),
95
- orderBy: { key: "asc" },
96
- });
97
- return roles.map(toRole);
98
- },
99
-
100
- async findRole(orgId, roleId) {
101
- const role = await db.authzRole.findFirst({
102
- where: { id: roleId, orgId: { in: [SYSTEM_ORG_ID, orgId] } },
103
- include: roleInclude(orgId),
104
- });
105
- return role ? toRole(role) : null;
106
- },
107
-
108
- async findRoleByKey(orgId, key) {
109
- const role = await db.authzRole.findFirst({ where: { key, orgId }, include: roleInclude(orgId) });
110
- return role ? toRole(role) : null;
111
- },
112
-
113
- async createRole(input) {
114
- const id = newId();
115
- await db.authzRole.create({
116
- data: {
117
- id,
118
- orgId: input.orgId,
119
- key: input.key,
120
- name: input.name,
121
- description: input.description,
122
- isSystem: input.isSystem,
123
- permissions: { create: grantRows(input.grants) },
124
- },
125
- });
126
- return id;
127
- },
128
-
129
- async updateRole(roleId, input) {
130
- await db.$transaction(async (tx) => {
131
- await tx.authzRole.update({
132
- where: { id: roleId },
133
- data: {
134
- ...(input.name === undefined ? {} : { name: input.name }),
135
- ...(input.description === undefined ? {} : { description: input.description }),
136
- },
137
- });
138
- if (input.grants) {
139
- await tx.authzRolePermission.deleteMany({ where: { roleId } });
140
- await tx.authzRolePermission.createMany({
141
- data: grantRows(input.grants).map((g) => ({ ...g, roleId })),
142
- });
143
- }
144
- });
145
- },
146
-
147
- async deleteRole(roleId) {
148
- await db.authzRole.delete({ where: { id: roleId } });
149
- },
150
-
151
- async memberRoles(orgId, userId) {
152
- const roles = await db.authzRole.findMany({
153
- where: { members: { some: { orgId, userId } } },
154
- include: roleInclude(orgId),
155
- });
156
- return roles.map(toRole);
157
- },
158
-
159
- async assignRole(orgId, userId, roleId, by) {
160
- const existing = await db.authzMemberRole.findFirst({ where: { orgId, userId, roleId } });
161
- if (existing) return false;
162
- await db.authzMemberRole.create({ data: { id: newId(), orgId, userId, roleId, assignedBy: by } });
163
- return true;
164
- },
165
-
166
- async revokeRole(orgId, userId, roleId) {
167
- const { count } = await db.authzMemberRole.deleteMany({ where: { orgId, userId, roleId } });
168
- return count > 0;
169
- },
170
-
171
- async removeMember(orgId, userId) {
172
- const [roles, overlays] = await db.$transaction([
173
- db.authzMemberRole.deleteMany({ where: { orgId, userId } }),
174
- db.authzOverlay.deleteMany({ where: { orgId, userId } }),
175
- ]);
176
- return { roles: roles.count, overlays: overlays.count };
177
- },
178
- async listOverlays(orgId, userId) {
179
- const rows = await db.authzOverlay.findMany({
180
- where: { orgId, userId },
181
- orderBy: { createdAt: "desc" },
182
- });
183
- return rows.map(toOverlay);
184
- },
185
-
186
- async addOverlay(input) {
187
- const id = newId();
188
- const scope = input.scope ?? "";
189
- await db.authzOverlay.deleteMany({
190
- where: {
191
- orgId: input.orgId,
192
- userId: input.userId,
193
- resource: input.resource,
194
- action: input.action,
195
- scope,
196
- effect: input.effect,
197
- },
198
- });
199
- await db.authzOverlay.create({ data: { ...input, id, scope } });
200
- return id;
201
- },
202
-
203
- async removeOverlay(orgId, userId, overlayId) {
204
- const row = await db.authzOverlay.findFirst({ where: { id: overlayId, orgId, userId } });
205
- if (!row) return null;
206
- await db.authzOverlay.deleteMany({ where: { id: overlayId, orgId, userId } });
207
- return toOverlay(row);
208
- },
209
-
210
- async writeAudit(input) {
211
- await db.authzAuditLog.create({
212
- data: {
213
- id: newId(),
214
- orgId: input.orgId,
215
- actorId: input.actorId,
216
- targetUserId: input.targetUserId,
217
- changeType: input.changeType,
218
- before: input.before === undefined ? null : JSON.stringify(input.before),
219
- after: input.after === undefined ? null : JSON.stringify(input.after),
220
- },
221
- });
222
- },
223
-
224
- async listAudit(orgId, limit) {
225
- const rows = await db.authzAuditLog.findMany({
226
- where: { orgId },
227
- orderBy: { id: "desc" },
228
- take: limit,
229
- });
230
- return rows.map((r) => ({
231
- ...r,
232
- before: r.before ? (JSON.parse(r.before) as unknown) : null,
233
- after: r.after ? (JSON.parse(r.after) as unknown) : null,
234
- }));
235
- },
236
- };
@@ -1,12 +0,0 @@
1
- import { defineCommand } from "@tatara/core";
2
- import { env } from "#core/config/env";
3
- import { migrateDatabase } from "#core/db/migrate";
4
-
5
- export default defineCommand({
6
- name: "db:migrate",
7
- description: "Apply pending database migrations",
8
- async run(_args, { logger }) {
9
- await migrateDatabase(env.DATABASE_URL);
10
- logger.info("migrations applied");
11
- },
12
- });
@@ -1,19 +0,0 @@
1
- import { z } from "zod";
2
- import { defineCommand, flag } from "@tatara/core";
3
- import { env } from "#core/config/env";
4
- import { dropAllTables } from "#core/db/admin";
5
- import { migrateDatabase } from "#core/db/migrate";
6
-
7
- export default defineCommand({
8
- name: "db:reset",
9
- description: "Drop every table and re-apply migrations (refuses to run in production)",
10
- args: z.object({ force: flag() }),
11
- async run({ force }, { logger }) {
12
- if (env.NODE_ENV === "production" && !force) {
13
- throw new Error("db:reset refuses to run with NODE_ENV=production (pass --force to override)");
14
- }
15
- await dropAllTables(env.DATABASE_URL);
16
- await migrateDatabase(env.DATABASE_URL);
17
- logger.info("database reset");
18
- },
19
- });
@@ -1,13 +0,0 @@
1
- import { defineCommand } from "@tatara/core";
2
- import { seeds } from "#generated/seeds";
3
-
4
- export default defineCommand({
5
- name: "db:seed",
6
- description: "Run every idempotent seed (core first, then modules)",
7
- async run(_args, { logger }) {
8
- for (const seed of [...seeds].sort((a, b) => (a.order ?? 100) - (b.order ?? 100))) {
9
- await seed.run({ logger: logger.child({ seed: seed.name }) });
10
- logger.info({ seed: seed.name }, "seeded");
11
- }
12
- },
13
- });
@@ -1,7 +0,0 @@
1
- import { expect, it } from "vitest";
2
- import { parseFlags } from "@tatara/core";
3
- import reset from "#core/db/commands/reset";
4
-
5
- it("does not treat --force false as forcing db:reset", () => {
6
- expect(reset.args?.parse(parseFlags(["--force", "false"]))).toEqual({ force: false });
7
- });