@manablox/db 0.1.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.
Files changed (44) hide show
  1. package/README.md +21 -0
  2. package/dist/index-Cyf_N5K3.d.ts +658 -0
  3. package/dist/index-rZ24t-Ln.d.ts +4338 -0
  4. package/dist/index.d.ts +123 -0
  5. package/dist/index.js +60 -0
  6. package/dist/repositories-DYjzuuF6.js +1533 -0
  7. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  8. package/dist/schema-Bb4p16Yz.js +539 -0
  9. package/dist/schema.d.ts +2 -0
  10. package/dist/schema.js +2 -0
  11. package/dist/testing.d.ts +77 -0
  12. package/dist/testing.js +217 -0
  13. package/migrations/0005_menus.sql +44 -0
  14. package/migrations/0006_roles.sql +13 -0
  15. package/migrations/0007_apikey-permissions.sql +4 -0
  16. package/migrations/0008_workflows.sql +49 -0
  17. package/migrations/meta/0005_snapshot.json +2504 -0
  18. package/migrations/meta/0006_snapshot.json +2605 -0
  19. package/migrations/meta/0007_snapshot.json +2605 -0
  20. package/migrations/meta/0008_snapshot.json +2986 -0
  21. package/migrations/meta/_journal.json +28 -0
  22. package/package.json +22 -9
  23. package/drizzle.config.ts +0 -11
  24. package/src/bootstrap.ts +0 -13
  25. package/src/cli/migrate.ts +0 -24
  26. package/src/client.ts +0 -37
  27. package/src/columns.ts +0 -39
  28. package/src/index.ts +0 -13
  29. package/src/query.ts +0 -217
  30. package/src/repositories/asset-usage.ts +0 -166
  31. package/src/repositories/asset.ts +0 -189
  32. package/src/repositories/content-type.ts +0 -116
  33. package/src/repositories/content.ts +0 -758
  34. package/src/repositories/index.ts +0 -28
  35. package/src/repositories/space.ts +0 -78
  36. package/src/repositories/user.ts +0 -134
  37. package/src/schema.ts +0 -513
  38. package/test/asset-usage.test.ts +0 -101
  39. package/test/helpers.ts +0 -153
  40. package/test/publish.test.ts +0 -102
  41. package/test/query.test.ts +0 -140
  42. package/test/tree.test.ts +0 -188
  43. package/tsconfig.json +0 -4
  44. package/vitest.config.ts +0 -12
@@ -0,0 +1,217 @@
1
+ import { _ as applyBootstrapSql, g as createDatabase, t as createRepositories } from "./repositories-DYjzuuF6.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,44 @@
1
+ CREATE TABLE "menu_items" (
2
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3
+ "menu_id" uuid NOT NULL,
4
+ "parent_id" uuid,
5
+ "position" integer DEFAULT 0 NOT NULL,
6
+ "localization_id" uuid,
7
+ "label" text,
8
+ "url" text
9
+ );
10
+ --> statement-breakpoint
11
+ CREATE TABLE "menus" (
12
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
13
+ "space_id" uuid NOT NULL,
14
+ "name" text NOT NULL,
15
+ "machine_name" text NOT NULL,
16
+ "description" text,
17
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
18
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,
19
+ CONSTRAINT "menus_space_machine_name_key" UNIQUE("space_id","machine_name")
20
+ );
21
+ --> statement-breakpoint
22
+ ALTER TABLE "menu_items" ADD CONSTRAINT "menu_items_menu_id_menus_id_fk" FOREIGN KEY ("menu_id") REFERENCES "public"."menus"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
23
+ ALTER TABLE "menu_items" ADD CONSTRAINT "menu_items_parent_id_menu_items_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."menu_items"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
24
+ ALTER TABLE "menus" ADD CONSTRAINT "menus_space_id_spaces_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."spaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
25
+ CREATE INDEX "menu_items_menu_idx" ON "menu_items" USING btree ("menu_id","parent_id","position");--> statement-breakpoint
26
+ CREATE INDEX "menu_items_localization_idx" ON "menu_items" USING btree ("localization_id");--> statement-breakpoint
27
+ -- Every document that was flagged "show in menu" becomes an entry of a "Main" menu in
28
+ -- its space, one entry per document (all translations share it), nested the way the
29
+ -- documents nest in the tree. Entry ids equal the documents' localization ids for the
30
+ -- duration of the migration so parents can be linked in one pass.
31
+ INSERT INTO "menus" ("space_id", "name", "machine_name", "description")
32
+ SELECT "space_id", 'Main', 'main', 'Created from the former "show in menu" flag.'
33
+ FROM "contents" WHERE "visible_in_menu" GROUP BY "space_id";--> statement-breakpoint
34
+ INSERT INTO "menu_items" ("id", "menu_id", "parent_id", "position", "localization_id")
35
+ SELECT DISTINCT ON (c."localization_id") c."localization_id", m."id", NULL, c."position", c."localization_id"
36
+ FROM "contents" c JOIN "menus" m ON m."space_id" = c."space_id" AND m."machine_name" = 'main'
37
+ WHERE c."visible_in_menu"
38
+ ORDER BY c."localization_id", c."locale";--> statement-breakpoint
39
+ UPDATE "menu_items" mi SET "parent_id" = p."localization_id"
40
+ FROM "contents" c JOIN "contents" p ON p."id" = c."parent_id"
41
+ WHERE mi."id" = c."localization_id"
42
+ AND EXISTS (SELECT 1 FROM "menu_items" x WHERE x."id" = p."localization_id" AND x."menu_id" = mi."menu_id");--> statement-breakpoint
43
+ ALTER TABLE "contents" DROP COLUMN "visible_in_menu";--> statement-breakpoint
44
+ ALTER TABLE "published_contents" DROP COLUMN "visible_in_menu";
@@ -0,0 +1,13 @@
1
+ CREATE TABLE "roles" (
2
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3
+ "space_id" uuid NOT NULL,
4
+ "name" text NOT NULL,
5
+ "machine_name" text NOT NULL,
6
+ "description" text,
7
+ "permissions" jsonb DEFAULT '[]'::jsonb NOT NULL,
8
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
9
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
10
+ );
11
+ --> statement-breakpoint
12
+ ALTER TABLE "roles" ADD CONSTRAINT "roles_space_id_spaces_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."spaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
13
+ CREATE UNIQUE INDEX "roles_space_machine_name_key" ON "roles" USING btree ("space_id","machine_name");
@@ -0,0 +1,4 @@
1
+ -- The column was declared by better-auth's key schema and never written; it now holds
2
+ -- the key's grants as a JSON array. Anything that is not one becomes null.
3
+ ALTER TABLE "apikeys" ALTER COLUMN "permissions" SET DATA TYPE jsonb
4
+ USING (CASE WHEN "permissions" ~ '^\s*\[' THEN "permissions"::jsonb ELSE NULL END);
@@ -0,0 +1,49 @@
1
+ CREATE TABLE "push_subscriptions" (
2
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3
+ "user_id" uuid NOT NULL,
4
+ "endpoint" text NOT NULL,
5
+ "keys" jsonb NOT NULL,
6
+ "user_agent" text,
7
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
8
+ "last_used_at" timestamp with time zone,
9
+ CONSTRAINT "push_subscriptions_endpoint_key" UNIQUE("endpoint")
10
+ );
11
+ --> statement-breakpoint
12
+ CREATE TABLE "workflow_runs" (
13
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
14
+ "workflow_id" uuid NOT NULL,
15
+ "space_id" uuid NOT NULL,
16
+ "trigger" text NOT NULL,
17
+ "status" text DEFAULT 'queued' NOT NULL,
18
+ "context" jsonb NOT NULL,
19
+ "cursor" jsonb DEFAULT '[]'::jsonb NOT NULL,
20
+ "log" jsonb DEFAULT '[]'::jsonb NOT NULL,
21
+ "error" text,
22
+ "resume_at" timestamp with time zone,
23
+ "started_at" timestamp with time zone,
24
+ "finished_at" timestamp with time zone,
25
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL
26
+ );
27
+ --> statement-breakpoint
28
+ CREATE TABLE "workflows" (
29
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
30
+ "space_id" uuid NOT NULL,
31
+ "name" text NOT NULL,
32
+ "description" text,
33
+ "enabled" boolean DEFAULT false NOT NULL,
34
+ "trigger" jsonb NOT NULL,
35
+ "steps" jsonb DEFAULT '[]'::jsonb NOT NULL,
36
+ "last_scheduled_at" timestamp with time zone,
37
+ "last_run_at" timestamp with time zone,
38
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
39
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL
40
+ );
41
+ --> statement-breakpoint
42
+ ALTER TABLE "push_subscriptions" ADD CONSTRAINT "push_subscriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
43
+ ALTER TABLE "workflow_runs" ADD CONSTRAINT "workflow_runs_workflow_id_workflows_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "public"."workflows"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
44
+ ALTER TABLE "workflow_runs" ADD CONSTRAINT "workflow_runs_space_id_spaces_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."spaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
45
+ ALTER TABLE "workflows" ADD CONSTRAINT "workflows_space_id_spaces_id_fk" FOREIGN KEY ("space_id") REFERENCES "public"."spaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
46
+ CREATE INDEX "push_subscriptions_user_idx" ON "push_subscriptions" USING btree ("user_id");--> statement-breakpoint
47
+ CREATE INDEX "workflow_runs_workflow_idx" ON "workflow_runs" USING btree ("workflow_id","created_at");--> statement-breakpoint
48
+ CREATE INDEX "workflow_runs_resume_idx" ON "workflow_runs" USING btree ("status","resume_at");--> statement-breakpoint
49
+ CREATE INDEX "workflows_space_idx" ON "workflows" USING btree ("space_id","enabled");