@manablox/db 0.2.0 → 0.4.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.
Files changed (61) hide show
  1. package/dist/index-BLAkQMJT.d.ts +877 -0
  2. package/dist/index-DrNMGM9N.d.ts +5193 -0
  3. package/dist/index.d.ts +123 -0
  4. package/dist/index.js +60 -0
  5. package/dist/repositories-pz4NeWaF.js +1928 -0
  6. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  7. package/dist/schema-Dm3RcBst.js +648 -0
  8. package/dist/schema.d.ts +2 -0
  9. package/dist/schema.js +2 -0
  10. package/dist/testing.d.ts +77 -0
  11. package/dist/testing.js +217 -0
  12. package/migrations/0009_audit-log.sql +40 -0
  13. package/migrations/0010_notifications-approvals.sql +45 -0
  14. package/migrations/meta/0009_snapshot.json +3192 -0
  15. package/migrations/meta/0010_snapshot.json +3574 -0
  16. package/migrations/meta/_journal.json +14 -0
  17. package/package.json +18 -10
  18. package/drizzle.config.ts +0 -11
  19. package/src/bootstrap.ts +0 -13
  20. package/src/cli/create-db.ts +0 -30
  21. package/src/cli/migrate.ts +0 -17
  22. package/src/client.ts +0 -44
  23. package/src/columns.ts +0 -39
  24. package/src/errors.ts +0 -50
  25. package/src/index.ts +0 -19
  26. package/src/migrate.ts +0 -21
  27. package/src/pagination.ts +0 -52
  28. package/src/query.ts +0 -213
  29. package/src/repositories/asset-usage.ts +0 -166
  30. package/src/repositories/asset.ts +0 -181
  31. package/src/repositories/content-type.ts +0 -116
  32. package/src/repositories/content.ts +0 -811
  33. package/src/repositories/index.ts +0 -40
  34. package/src/repositories/menu.ts +0 -235
  35. package/src/repositories/role.ts +0 -85
  36. package/src/repositories/space.ts +0 -83
  37. package/src/repositories/user.ts +0 -280
  38. package/src/repositories/webhook.ts +0 -46
  39. package/src/repositories/workflow.ts +0 -306
  40. package/src/schema/assets.ts +0 -108
  41. package/src/schema/auth.ts +0 -166
  42. package/src/schema/content-types.ts +0 -31
  43. package/src/schema/content.ts +0 -133
  44. package/src/schema/index.ts +0 -38
  45. package/src/schema/menus.ts +0 -61
  46. package/src/schema/relations.ts +0 -64
  47. package/src/schema/spaces.ts +0 -20
  48. package/src/schema/webhooks.ts +0 -46
  49. package/src/schema/workflows.ts +0 -92
  50. package/src/testing-fixtures.ts +0 -139
  51. package/src/testing.ts +0 -105
  52. package/test/asset-usage.test.ts +0 -101
  53. package/test/menu.test.ts +0 -126
  54. package/test/publish.test.ts +0 -130
  55. package/test/query.test.ts +0 -170
  56. package/test/role.test.ts +0 -81
  57. package/test/tree.test.ts +0 -188
  58. package/test/user.test.ts +0 -126
  59. package/test/webhook.test.ts +0 -48
  60. package/tsconfig.json +0 -4
  61. package/vitest.config.ts +0 -10
@@ -0,0 +1,77 @@
1
+ import { it as DatabaseHandle, t as Repositories } from "./index-BLAkQMJT.js";
2
+ import { ContentTypeDefinition, ContentTypeRegistry } from "@manablox/core";
3
+ //#region src/testing-fixtures.d.ts
4
+ export declare const stringField: import("@manablox/core").FieldTypeDefinition<Record<string, unknown>, string>;
5
+ export declare const numberField: import("@manablox/core").FieldTypeDefinition<Record<string, unknown>, number>;
6
+ export interface RepositoryTestContext {
7
+ handle: DatabaseHandle;
8
+ /** Every statement sent to Postgres, so a test can assert what a call cost. */
9
+ queries: string[];
10
+ repos: Repositories;
11
+ registry: ContentTypeRegistry;
12
+ types: {
13
+ page: ContentTypeDefinition;
14
+ folder: ContentTypeDefinition;
15
+ };
16
+ spaceId: string;
17
+ close: () => Promise<void>;
18
+ }
19
+ /** Spins up an isolated database per suite, migrated from the real migration files. */
20
+ export declare function createRepositoryContext(name: string): Promise<RepositoryTestContext>;
21
+ /** Convenience creator so tests read as tree shapes rather than field soup. */
22
+ export declare function makeNode(ctx: RepositoryTestContext, options: {
23
+ title: string;
24
+ slug: string;
25
+ parentId?: string | null;
26
+ type?: 'page' | 'folder';
27
+ fields?: Record<string, unknown>;
28
+ }): Promise<{
29
+ createdAt: Date;
30
+ createdBy: string | null;
31
+ fields: Record<string, unknown>;
32
+ id: string;
33
+ locale: string;
34
+ localizationId: string;
35
+ parentId: string | null;
36
+ path: string;
37
+ permalink: string | null;
38
+ permalinkPath: string;
39
+ permalinkSegment: string | null;
40
+ position: number;
41
+ publishedAt: Date | null;
42
+ search: string | null;
43
+ searchText: string;
44
+ slug: string;
45
+ spaceId: string;
46
+ status: import("@manablox/core").ContentStatus;
47
+ title: string;
48
+ typeId: string;
49
+ updatedAt: Date;
50
+ updatedBy: string | null;
51
+ version: number;
52
+ }>;
53
+ //#endregion
54
+ //#region src/testing.d.ts
55
+ /**
56
+ * Only ever connected to in order to `create database`: every test file works in its own
57
+ * fresh database and drops it again, so this points at a Postgres *server*, not at data
58
+ * the tests touch. That is why falling back to `DATABASE_URL` is safe — it means the
59
+ * suite runs wherever the app itself runs (a container, CI, the host) without a second
60
+ * variable that has to be kept in step. `TEST_DATABASE_URL` still overrides it.
61
+ */
62
+ export declare const TEST_ADMIN_URL: string;
63
+ export declare function withDatabase(url: string, name: string): string;
64
+ export interface TestDatabase {
65
+ name: string;
66
+ url: string;
67
+ /** Drops the database. Safe to call once every connection to it is closed. */
68
+ drop: () => Promise<void>;
69
+ }
70
+ /**
71
+ * A fresh, migrated database for one test suite, cloned from the template.
72
+ *
73
+ * `prefix` names the suite in `pg_database`, which is what you grep for when a run was
74
+ * interrupted and left a database behind.
75
+ */
76
+ export declare function createTestDatabase(prefix: string): Promise<TestDatabase>;
77
+ //#endregion
@@ -0,0 +1,217 @@
1
+ import { b as applyBootstrapSql, t as createRepositories, y as createDatabase } from "./repositories-pz4NeWaF.js";
2
+ import postgres from "postgres";
3
+ import { ContentTypeRegistry, FieldTypeRegistry, defineContentType, defineFieldType } from "@manablox/core";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { migrate } from "drizzle-orm/postgres-js/migrator";
7
+ import { createHash } from "node:crypto";
8
+ import { readFileSync, readdirSync } from "node:fs";
9
+ //#region src/testing-fixtures.ts
10
+ /**
11
+ * Fixtures for a repository-level test: two plain field types, a `page` and a `folder`
12
+ * content type, a migrated database with one space, and a node maker. Shared through
13
+ * `@manablox/db/testing` so a suite in another package can start from the same world.
14
+ */
15
+ const anySchema = () => ({ "~standard": {
16
+ version: 1,
17
+ vendor: "test",
18
+ validate: (value) => ({ value })
19
+ } });
20
+ const stringField = defineFieldType({
21
+ name: "string",
22
+ label: "Text",
23
+ settingsSchema: anySchema(),
24
+ valueSchema: () => anySchema(),
25
+ defaultValue: () => "",
26
+ storage: {
27
+ kind: "jsonb",
28
+ index: "btree"
29
+ },
30
+ filters: [
31
+ "eq",
32
+ "neq",
33
+ "contains",
34
+ "startsWith",
35
+ "in",
36
+ "isNull",
37
+ "isNotNull"
38
+ ],
39
+ graphql: { type: {
40
+ kind: "scalar",
41
+ name: "String"
42
+ } },
43
+ search: (value) => typeof value === "string" ? value : null,
44
+ admin: { input: "string" }
45
+ });
46
+ const numberField = defineFieldType({
47
+ name: "number",
48
+ label: "Number",
49
+ settingsSchema: anySchema(),
50
+ valueSchema: () => anySchema(),
51
+ defaultValue: () => 0,
52
+ storage: {
53
+ kind: "jsonb",
54
+ index: "btree"
55
+ },
56
+ filters: [
57
+ "eq",
58
+ "lt",
59
+ "lte",
60
+ "gt",
61
+ "gte"
62
+ ],
63
+ graphql: { type: {
64
+ kind: "scalar",
65
+ name: "Float"
66
+ } },
67
+ admin: { input: "number" }
68
+ });
69
+ /** Spins up an isolated database per suite, migrated from the real migration files. */
70
+ async function createRepositoryContext(name) {
71
+ const database = await createTestDatabase(name);
72
+ const queries = [];
73
+ const handle = createDatabase({
74
+ url: database.url,
75
+ max: 4
76
+ }, { onQuery: (query) => queries.push(query) });
77
+ const fieldTypes = new FieldTypeRegistry();
78
+ fieldTypes.register(stringField);
79
+ fieldTypes.register(numberField);
80
+ const page = defineContentType({
81
+ name: "page",
82
+ fields: [{
83
+ name: "body",
84
+ type: "string"
85
+ }, {
86
+ name: "weight",
87
+ type: "number"
88
+ }]
89
+ });
90
+ const folder = defineContentType({
91
+ name: "folder",
92
+ hasSlug: false,
93
+ fields: [{
94
+ name: "note",
95
+ type: "string"
96
+ }]
97
+ });
98
+ const registry = new ContentTypeRegistry(fieldTypes);
99
+ registry.setAll([page, folder]);
100
+ registry.validate();
101
+ const repos = createRepositories(handle.db, registry);
102
+ const space = await repos.spaces.create({
103
+ name: "Test",
104
+ machineName: "test",
105
+ url: "http://localhost:3002"
106
+ });
107
+ return {
108
+ handle,
109
+ queries,
110
+ repos,
111
+ registry,
112
+ types: {
113
+ page,
114
+ folder
115
+ },
116
+ spaceId: space.id,
117
+ close: async () => {
118
+ await handle.close();
119
+ await database.drop();
120
+ }
121
+ };
122
+ }
123
+ /** Convenience creator so tests read as tree shapes rather than field soup. */
124
+ async function makeNode(ctx, options) {
125
+ const type = ctx.types[options.type ?? "page"];
126
+ const searchText = Object.values(options.fields ?? {}).filter((value) => typeof value === "string").join(" ");
127
+ return ctx.repos.content.create({
128
+ searchText,
129
+ spaceId: ctx.spaceId,
130
+ typeId: type.id,
131
+ locale: "en",
132
+ parentId: options.parentId ?? null,
133
+ title: options.title,
134
+ slug: options.slug,
135
+ fields: options.fields ?? {},
136
+ hasSlug: type.hasSlug
137
+ });
138
+ }
139
+ //#endregion
140
+ //#region src/testing.ts
141
+ const MIGRATIONS = resolve(dirname(fileURLToPath(import.meta.url)), "../migrations");
142
+ /**
143
+ * Only ever connected to in order to `create database`: every test file works in its own
144
+ * fresh database and drops it again, so this points at a Postgres *server*, not at data
145
+ * the tests touch. That is why falling back to `DATABASE_URL` is safe — it means the
146
+ * suite runs wherever the app itself runs (a container, CI, the host) without a second
147
+ * variable that has to be kept in step. `TEST_DATABASE_URL` still overrides it.
148
+ */
149
+ const TEST_ADMIN_URL = process.env.TEST_DATABASE_URL ?? process.env.DATABASE_URL ?? "postgres://manablox:manablox@localhost:5432/manablox";
150
+ /** A fingerprint of the migration files, so a schema change gets a fresh template. */
151
+ function migrationsFingerprint() {
152
+ const hash = createHash("sha1");
153
+ for (const file of readdirSync(MIGRATIONS).sort()) if (file.endsWith(".sql")) hash.update(readFileSync(join(MIGRATIONS, file)));
154
+ return hash.digest("hex").slice(0, 10);
155
+ }
156
+ const TEMPLATE = `manablox_test_template_${migrationsFingerprint()}`;
157
+ /**
158
+ * Makes sure the migrated template exists, once per server.
159
+ *
160
+ * Migrating takes a second or two; `create database … template …` takes milliseconds,
161
+ * so every suite clones the template instead of migrating from scratch. Suites run in
162
+ * parallel across processes, so the check-and-create is serialised on a session-level
163
+ * advisory lock — the second process finds the template already there.
164
+ */
165
+ async function ensureTemplate(admin) {
166
+ await admin.unsafe(`select pg_advisory_lock(hashtext('${TEMPLATE}'))`);
167
+ try {
168
+ const [row] = await admin.unsafe(`select 1 as found from pg_database where datname = '${TEMPLATE}'`);
169
+ if (row) return;
170
+ await admin.unsafe(`create database "${TEMPLATE}"`);
171
+ const handle = createDatabase({
172
+ url: withDatabase(TEST_ADMIN_URL, TEMPLATE),
173
+ max: 1
174
+ });
175
+ try {
176
+ await applyBootstrapSql(handle.sql);
177
+ await migrate(handle.db, { migrationsFolder: MIGRATIONS });
178
+ } finally {
179
+ await handle.close();
180
+ }
181
+ } finally {
182
+ await admin.unsafe(`select pg_advisory_unlock(hashtext('${TEMPLATE}'))`);
183
+ }
184
+ }
185
+ function withDatabase(url, name) {
186
+ return url.replace(/\/[^/?]*(\?.*)?$/, `/${name}$1`);
187
+ }
188
+ /**
189
+ * A fresh, migrated database for one test suite, cloned from the template.
190
+ *
191
+ * `prefix` names the suite in `pg_database`, which is what you grep for when a run was
192
+ * interrupted and left a database behind.
193
+ */
194
+ async function createTestDatabase(prefix) {
195
+ const name = `manablox_test_${prefix}_${Date.now().toString(36)}_${process.pid.toString(36)}`;
196
+ const admin = postgres(TEST_ADMIN_URL, { max: 1 });
197
+ try {
198
+ await ensureTemplate(admin);
199
+ await admin.unsafe(`create database "${name}" template "${TEMPLATE}"`);
200
+ } finally {
201
+ await admin.end();
202
+ }
203
+ return {
204
+ name,
205
+ url: withDatabase(TEST_ADMIN_URL, name),
206
+ drop: async () => {
207
+ const cleanup = postgres(TEST_ADMIN_URL, { max: 1 });
208
+ try {
209
+ await cleanup.unsafe(`drop database if exists "${name}" with (force)`);
210
+ } finally {
211
+ await cleanup.end();
212
+ }
213
+ }
214
+ };
215
+ }
216
+ //#endregion
217
+ export { TEST_ADMIN_URL, createRepositoryContext, createTestDatabase, makeNode, numberField, stringField, withDatabase };
@@ -0,0 +1,40 @@
1
+ CREATE TABLE "audit_entries" (
2
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3
+ "seq" bigserial NOT NULL,
4
+ "at" timestamp with time zone DEFAULT now() NOT NULL,
5
+ "space_id" uuid,
6
+ "actor_kind" text NOT NULL,
7
+ "actor_id" text,
8
+ "actor_label" text NOT NULL,
9
+ "actor_detail" jsonb,
10
+ "action" text NOT NULL,
11
+ "target_kind" text NOT NULL,
12
+ "target_id" text,
13
+ "target_label" text,
14
+ "changes" jsonb DEFAULT '[]'::jsonb NOT NULL,
15
+ "meta" jsonb,
16
+ "prev_hash" text,
17
+ "hash" text NOT NULL
18
+ );
19
+ --> statement-breakpoint
20
+ CREATE INDEX "audit_entries_seq_idx" ON "audit_entries" USING btree ("seq");--> statement-breakpoint
21
+ CREATE INDEX "audit_entries_space_at_idx" ON "audit_entries" USING btree ("space_id","at");--> statement-breakpoint
22
+ CREATE INDEX "audit_entries_target_idx" ON "audit_entries" USING btree ("target_kind","target_id");--> statement-breakpoint
23
+ CREATE INDEX "audit_entries_actor_idx" ON "audit_entries" USING btree ("actor_kind","actor_id");--> statement-breakpoint
24
+ CREATE INDEX "audit_entries_action_idx" ON "audit_entries" USING btree ("action");--> statement-breakpoint
25
+ -- The log is append-only. Postgres has no "insert-only" grant short of a second role,
26
+ -- so a trigger refuses every update and delete instead; dropping it is a deliberate,
27
+ -- visible act rather than a stray statement. Not tracked by drizzle-kit: `pnpm
28
+ -- db:generate` neither sees nor removes it.
29
+ CREATE FUNCTION audit_entries_immutable() RETURNS trigger AS $$
30
+ BEGIN
31
+ RAISE EXCEPTION 'audit_entries is append-only: % refused', TG_OP
32
+ USING ERRCODE = 'insufficient_privilege';
33
+ END;
34
+ $$ LANGUAGE plpgsql;--> statement-breakpoint
35
+ CREATE TRIGGER audit_entries_immutable
36
+ BEFORE UPDATE OR DELETE ON "audit_entries"
37
+ FOR EACH ROW EXECUTE FUNCTION audit_entries_immutable();--> statement-breakpoint
38
+ CREATE TRIGGER audit_entries_no_truncate
39
+ BEFORE TRUNCATE ON "audit_entries"
40
+ FOR EACH STATEMENT EXECUTE FUNCTION audit_entries_immutable();
@@ -0,0 +1,45 @@
1
+ CREATE TABLE "content_approvals" (
2
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3
+ "space_id" uuid NOT NULL,
4
+ "content_id" uuid NOT NULL,
5
+ "type_id" uuid NOT NULL,
6
+ "status" text DEFAULT 'pending' NOT NULL,
7
+ "requested_by" uuid,
8
+ "requested_by_label" text DEFAULT '' NOT NULL,
9
+ "request_note" text,
10
+ "content_version" integer,
11
+ "requested_at" timestamp with time zone DEFAULT now() NOT NULL,
12
+ "decided_by" uuid,
13
+ "decided_by_label" text,
14
+ "decision_note" text,
15
+ "decided_at" timestamp with time zone
16
+ );
17
+ --> statement-breakpoint
18
+ CREATE TABLE "notifications" (
19
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
20
+ "user_id" uuid NOT NULL,
21
+ "space_id" uuid,
22
+ "kind" text NOT NULL,
23
+ "title" text NOT NULL,
24
+ "body" text DEFAULT '' NOT NULL,
25
+ "url" text,
26
+ "target_kind" text,
27
+ "target_id" text,
28
+ "actor_id" text,
29
+ "actor_label" text,
30
+ "meta" jsonb,
31
+ "read_at" timestamp with time zone,
32
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
33
+ );
34
+ --> statement-breakpoint
35
+ ALTER TABLE "users" ADD COLUMN "notification_preferences" jsonb DEFAULT '{}'::jsonb NOT NULL;--> statement-breakpoint
36
+ ALTER TABLE "content_types" ADD COLUMN "requires_approval" boolean DEFAULT false NOT NULL;--> statement-breakpoint
37
+ ALTER TABLE "content_approvals" ADD CONSTRAINT "content_approvals_space_id_spaces_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."spaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
38
+ ALTER TABLE "content_approvals" ADD CONSTRAINT "content_approvals_content_id_contents_id_fk" FOREIGN KEY ("content_id") REFERENCES "public"."contents"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
39
+ ALTER TABLE "notifications" ADD CONSTRAINT "notifications_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
40
+ ALTER TABLE "notifications" ADD CONSTRAINT "notifications_space_id_spaces_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."spaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
41
+ CREATE INDEX "content_approvals_content_idx" ON "content_approvals" USING btree ("content_id","requested_at");--> statement-breakpoint
42
+ CREATE INDEX "content_approvals_space_status_idx" ON "content_approvals" USING btree ("space_id","status","requested_at");--> statement-breakpoint
43
+ CREATE INDEX "notifications_user_created_idx" ON "notifications" USING btree ("user_id","created_at");--> statement-breakpoint
44
+ CREATE INDEX "notifications_user_unread_idx" ON "notifications" USING btree ("user_id","read_at");--> statement-breakpoint
45
+ CREATE INDEX "notifications_target_idx" ON "notifications" USING btree ("target_kind","target_id");