@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/dist/testing.js
ADDED
|
@@ -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 };
|
package/package.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manablox/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
|
-
"types": "./
|
|
8
|
-
"default": "./
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"default": "./dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"./schema": {
|
|
11
|
-
"types": "./
|
|
12
|
-
"default": "./
|
|
11
|
+
"types": "./dist/schema.d.ts",
|
|
12
|
+
"default": "./dist/schema.js"
|
|
13
13
|
},
|
|
14
14
|
"./testing": {
|
|
15
|
-
"types": "./
|
|
16
|
-
"default": "./
|
|
15
|
+
"types": "./dist/testing.d.ts",
|
|
16
|
+
"default": "./dist/testing.js"
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
|
-
"main": "./
|
|
20
|
-
"types": "./
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@manablox/core": "0.
|
|
22
|
+
"@manablox/core": "0.3.0",
|
|
23
23
|
"drizzle-orm": "^0.45.2",
|
|
24
24
|
"postgres": "^3.4.9"
|
|
25
25
|
},
|
|
@@ -28,11 +28,19 @@
|
|
|
28
28
|
"@types/node": "^26.4.1",
|
|
29
29
|
"drizzle-kit": "^0.31.10",
|
|
30
30
|
"testcontainers": "^12.1.0",
|
|
31
|
+
"tsdown": "^0.23.0",
|
|
31
32
|
"tsx": "^4.20.7",
|
|
32
33
|
"typescript": "^7.0.2",
|
|
33
34
|
"vitest": "^5.0.0"
|
|
34
35
|
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"!dist/**/*.map",
|
|
39
|
+
"migrations",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
35
42
|
"scripts": {
|
|
43
|
+
"build": "tsdown",
|
|
36
44
|
"generate": "drizzle-kit generate",
|
|
37
45
|
"migrate": "tsx src/cli/migrate.ts",
|
|
38
46
|
"typecheck": "tsc --noEmit",
|
package/drizzle.config.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'drizzle-kit';
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
schema: './src/schema/index.ts',
|
|
5
|
-
out: './migrations',
|
|
6
|
-
dialect: 'postgresql',
|
|
7
|
-
dbCredentials: {
|
|
8
|
-
url: process.env.DATABASE_URL ?? 'postgres://manablox:manablox@localhost:5432/manablox',
|
|
9
|
-
},
|
|
10
|
-
casing: 'snake_case',
|
|
11
|
-
});
|
package/src/bootstrap.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { Sql } from './client.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Extensions the schema depends on. Run before the generated migrations, which
|
|
5
|
-
* reference `ltree` columns and `pg_trgm` operator classes.
|
|
6
|
-
*/
|
|
7
|
-
export async function applyBootstrapSql(sql: Sql): Promise<void> {
|
|
8
|
-
await sql.unsafe(`
|
|
9
|
-
create extension if not exists "ltree";
|
|
10
|
-
create extension if not exists "pg_trgm";
|
|
11
|
-
create extension if not exists "btree_gin";
|
|
12
|
-
`);
|
|
13
|
-
}
|
package/src/cli/create-db.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import postgres from 'postgres';
|
|
2
|
-
import { withDatabase } from '../testing.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Creates the database `DATABASE_URL` names, on the server it names, if it does not
|
|
6
|
-
* exist — `--fresh` drops it first. For throwaway databases (the end-to-end run) where
|
|
7
|
-
* nothing else provisions them.
|
|
8
|
-
*/
|
|
9
|
-
const url = process.env.DATABASE_URL;
|
|
10
|
-
if (!url) {
|
|
11
|
-
console.error('DATABASE_URL is required');
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const name = new URL(url).pathname.replace(/^\//, '');
|
|
16
|
-
const fresh = process.argv.includes('--fresh');
|
|
17
|
-
const admin = postgres(withDatabase(url, 'postgres'), { max: 1 });
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
if (fresh) await admin.unsafe(`drop database if exists "${name}" with (force)`);
|
|
21
|
-
const [row] = await admin.unsafe(`select 1 as found from pg_database where datname = '${name}'`);
|
|
22
|
-
if (!row) {
|
|
23
|
-
await admin.unsafe(`create database "${name}"`);
|
|
24
|
-
console.info(`created database ${name}`);
|
|
25
|
-
} else {
|
|
26
|
-
console.info(`database ${name} exists`);
|
|
27
|
-
}
|
|
28
|
-
} finally {
|
|
29
|
-
await admin.end();
|
|
30
|
-
}
|
package/src/cli/migrate.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { createDatabase } from '../client.js';
|
|
2
|
-
import { runMigrations } from '../migrate.js';
|
|
3
|
-
|
|
4
|
-
const url = process.env.DATABASE_URL;
|
|
5
|
-
if (!url) {
|
|
6
|
-
console.error('DATABASE_URL is required');
|
|
7
|
-
process.exit(1);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const handle = createDatabase({ url, max: 1 });
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
await runMigrations(handle);
|
|
14
|
-
console.info('migrations applied');
|
|
15
|
-
} finally {
|
|
16
|
-
await handle.close();
|
|
17
|
-
}
|
package/src/client.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { DatabaseConfig } from '@manablox/core';
|
|
2
|
-
import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
3
|
-
import postgres from 'postgres';
|
|
4
|
-
import * as schema from './schema/index.js';
|
|
5
|
-
|
|
6
|
-
export type Database = PostgresJsDatabase<typeof schema>;
|
|
7
|
-
/** What `db.transaction((tx) => …)` hands its callback. */
|
|
8
|
-
export type Transaction = Parameters<Parameters<Database['transaction']>[0]>[0];
|
|
9
|
-
/**
|
|
10
|
-
* Anything a query can run on. Repository internals take this so the same helper serves
|
|
11
|
-
* a call inside a transaction and one outside it, without casting `tx` to `Database`.
|
|
12
|
-
*/
|
|
13
|
-
export type Executor = Database | Transaction;
|
|
14
|
-
export type Sql = ReturnType<typeof postgres>;
|
|
15
|
-
|
|
16
|
-
export interface DatabaseHandle {
|
|
17
|
-
db: Database;
|
|
18
|
-
sql: Sql;
|
|
19
|
-
close: () => Promise<void>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface DatabaseOptions {
|
|
23
|
-
/** Called once per statement sent to Postgres. Used by tests to assert query counts. */
|
|
24
|
-
onQuery?: (query: string) => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function createDatabase(
|
|
28
|
-
config: DatabaseConfig,
|
|
29
|
-
options: DatabaseOptions = {},
|
|
30
|
-
): DatabaseHandle {
|
|
31
|
-
const sql = postgres(config.url, {
|
|
32
|
-
max: config.max ?? 10,
|
|
33
|
-
...(options.onQuery ? { debug: (_c: number, query: string) => options.onQuery?.(query) } : {}),
|
|
34
|
-
...(config.ssl ? { ssl: 'require' as const } : {}),
|
|
35
|
-
// `ltree` and `tsvector` have no client-side parser, so postgres.js returns them as
|
|
36
|
-
// strings — which is exactly what the schema declares. Do not pass `types: {}` here:
|
|
37
|
-
// it replaces the built-in serialisers wholesale and Date parameters stop working.
|
|
38
|
-
onnotice: () => {},
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const db = drizzle(sql, { schema, casing: 'snake_case' });
|
|
42
|
-
|
|
43
|
-
return { db, sql, close: () => sql.end({ timeout: 5 }) };
|
|
44
|
-
}
|
package/src/columns.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { customType } from 'drizzle-orm/pg-core';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* `ltree` — the materialised ancestor path of a content node. A subtree move is one
|
|
5
|
-
* `UPDATE ... SET path = :newParent || subpath(path, nlevel(:oldParent))`.
|
|
6
|
-
*/
|
|
7
|
-
export const ltree = customType<{ data: string; driverData: string }>({
|
|
8
|
-
dataType: () => 'ltree',
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
/** `tsvector` — generated from `title` + `search_text`, never written directly. */
|
|
12
|
-
export const tsvector = customType<{ data: string; driverData: string }>({
|
|
13
|
-
dataType: () => 'tsvector',
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* ltree labels accept only `[A-Za-z0-9_]`, so UUID hyphens are swapped for underscores.
|
|
18
|
-
* The transform is total and reversible.
|
|
19
|
-
*/
|
|
20
|
-
export const idToLabel = (id: string): string => id.replaceAll('-', '_');
|
|
21
|
-
export const labelToId = (label: string): string => {
|
|
22
|
-
const hex = label.replaceAll('_', '');
|
|
23
|
-
return [
|
|
24
|
-
hex.slice(0, 8),
|
|
25
|
-
hex.slice(8, 12),
|
|
26
|
-
hex.slice(12, 16),
|
|
27
|
-
hex.slice(16, 20),
|
|
28
|
-
hex.slice(20, 32),
|
|
29
|
-
].join('-');
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
/** Builds the path of a node from its ancestors' ids (root first) plus its own. */
|
|
33
|
-
export const buildPath = (ancestorIds: string[], selfId: string): string =>
|
|
34
|
-
[...ancestorIds, selfId].map(idToLabel).join('.');
|
|
35
|
-
|
|
36
|
-
/** Splits a stored path back into ids, root first, self last. */
|
|
37
|
-
export const parsePath = (path: string): string[] => path.split('.').filter(Boolean).map(labelToId);
|
|
38
|
-
|
|
39
|
-
export const pathDepth = (path: string): number => (path ? path.split('.').length : 0);
|
package/src/errors.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { type ErrorDetail, type ErrorKey, ManabloxError } from '@manablox/core';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Postgres reports a unique-constraint violation as SQLSTATE `23505` with the constraint
|
|
5
|
-
* name in the message. Drizzle wraps the driver error, so the code and the message sit on
|
|
6
|
-
* `cause`, while the outer message is only ever "Failed query: insert into …".
|
|
7
|
-
*/
|
|
8
|
-
export function isUniqueViolation(error: unknown, constraint: string): boolean {
|
|
9
|
-
const cause = (error as { cause?: { code?: string; message?: string } })?.cause;
|
|
10
|
-
const code = (error as { code?: string })?.code ?? cause?.code;
|
|
11
|
-
const detail = `${cause?.message ?? ''} ${(error as { message?: string })?.message ?? ''}`;
|
|
12
|
-
return code === '23505' && detail.includes(constraint);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface UniqueViolationMapping {
|
|
16
|
-
/** The constraint name, or a distinctive part of it. */
|
|
17
|
-
constraint: string;
|
|
18
|
-
/** The detail reported for the field, e.g. `content.slug.duplicate`. */
|
|
19
|
-
key: ErrorKey;
|
|
20
|
-
path?: (string | number)[];
|
|
21
|
-
params?: Record<string, unknown>;
|
|
22
|
-
/** The error's own key. */
|
|
23
|
-
errorKey: ErrorKey;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Turns a unique violation on one constraint into the validation error the client would
|
|
28
|
-
* have received had the check run before the write, and rethrows anything else untouched.
|
|
29
|
-
* Use as `.catch((error) => rethrowUniqueViolation(error, { … }))`.
|
|
30
|
-
*/
|
|
31
|
-
export function rethrowUniqueViolation(error: unknown, mapping: UniqueViolationMapping): never {
|
|
32
|
-
if (!isUniqueViolation(error, mapping.constraint)) throw error;
|
|
33
|
-
|
|
34
|
-
const detail: ErrorDetail = {
|
|
35
|
-
key: mapping.key,
|
|
36
|
-
...(mapping.path ? { path: mapping.path } : {}),
|
|
37
|
-
...(mapping.params ? { params: mapping.params } : {}),
|
|
38
|
-
};
|
|
39
|
-
throw ManabloxError.validation([detail], mapping.errorKey);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* A preset for one constraint: `const taken = uniqueViolation({ … })` once beside the
|
|
44
|
-
* service, then `.catch(taken({ machineName }))` on every write that can trip it. The
|
|
45
|
-
* params are the only part that differs per call, so they are the only argument left.
|
|
46
|
-
*/
|
|
47
|
-
export function uniqueViolation(preset: Omit<UniqueViolationMapping, 'params'>) {
|
|
48
|
-
return (params: Record<string, unknown>) => (error: unknown) =>
|
|
49
|
-
rethrowUniqueViolation(error, { ...preset, params });
|
|
50
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export * from './bootstrap.js';
|
|
2
|
-
export * from './client.js';
|
|
3
|
-
export * from './columns.js';
|
|
4
|
-
export * from './errors.js';
|
|
5
|
-
export * from './migrate.js';
|
|
6
|
-
export * from './pagination.js';
|
|
7
|
-
export * from './query.js';
|
|
8
|
-
export * from './repositories/asset.js';
|
|
9
|
-
export * from './repositories/asset-usage.js';
|
|
10
|
-
export * from './repositories/content.js';
|
|
11
|
-
export * from './repositories/content-type.js';
|
|
12
|
-
export * from './repositories/index.js';
|
|
13
|
-
export * from './repositories/menu.js';
|
|
14
|
-
export * from './repositories/space.js';
|
|
15
|
-
export * from './repositories/user.js';
|
|
16
|
-
export * from './repositories/webhook.js';
|
|
17
|
-
export * from './repositories/workflow.js';
|
|
18
|
-
export * as schema from './schema/index.js';
|
|
19
|
-
export * from './schema/index.js';
|
package/src/migrate.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { dirname, resolve } from 'node:path';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { migrate } from 'drizzle-orm/postgres-js/migrator';
|
|
4
|
-
import { applyBootstrapSql } from './bootstrap.js';
|
|
5
|
-
import type { DatabaseHandle } from './client.js';
|
|
6
|
-
|
|
7
|
-
/** The SQL files shipped with this package, next to `src/`. */
|
|
8
|
-
export const MIGRATIONS_FOLDER = resolve(dirname(fileURLToPath(import.meta.url)), '../migrations');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Brings a database up to the schema this version of the package expects.
|
|
12
|
-
*
|
|
13
|
-
* The same call the compose stack's `migrate` service and the `manablox migrate` CLI
|
|
14
|
-
* make, so an instance run from a dependency and one run from the repository apply the
|
|
15
|
-
* same files in the same order. Idempotent: drizzle records what it has applied.
|
|
16
|
-
*/
|
|
17
|
-
export async function runMigrations(handle: DatabaseHandle): Promise<void> {
|
|
18
|
-
// Extensions must exist before the generated migrations reference ltree columns.
|
|
19
|
-
await applyBootstrapSql(handle.sql);
|
|
20
|
-
await migrate(handle.db, { migrationsFolder: MIGRATIONS_FOLDER });
|
|
21
|
-
}
|
package/src/pagination.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { getTableColumns, type SQL, sql } from 'drizzle-orm';
|
|
2
|
-
import type { PgColumn, PgTable } from 'drizzle-orm/pg-core';
|
|
3
|
-
import type { Executor } from './client.js';
|
|
4
|
-
import type { Pagination } from './query.js';
|
|
5
|
-
|
|
6
|
-
export interface Paginated<T> {
|
|
7
|
-
items: T[];
|
|
8
|
-
total: number;
|
|
9
|
-
limit: number;
|
|
10
|
-
offset: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* One page of a table plus the total, in one round trip: a window `count(*) over ()`
|
|
15
|
-
* rides along with the rows. A page past the end comes back empty and so carries no
|
|
16
|
-
* count; only then is the total asked for separately, so a caller paging by `total`
|
|
17
|
-
* still learns the true size.
|
|
18
|
-
*/
|
|
19
|
-
export async function paginate<TTable extends PgTable>(
|
|
20
|
-
db: Executor,
|
|
21
|
-
table: TTable,
|
|
22
|
-
options: {
|
|
23
|
-
where?: SQL | undefined;
|
|
24
|
-
orderBy: SQL | PgColumn | (SQL | PgColumn)[];
|
|
25
|
-
pagination: Pagination;
|
|
26
|
-
},
|
|
27
|
-
): Promise<Paginated<TTable['$inferSelect']>> {
|
|
28
|
-
const orderBy = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
|
|
29
|
-
const rows = (await db
|
|
30
|
-
.select({ ...getTableColumns(table), total: sql<number>`count(*) over ()::int` })
|
|
31
|
-
.from(table as PgTable)
|
|
32
|
-
.where(options.where)
|
|
33
|
-
.orderBy(...orderBy)
|
|
34
|
-
.limit(options.pagination.limit)
|
|
35
|
-
.offset(options.pagination.offset)) as Array<TTable['$inferSelect'] & { total: number }>;
|
|
36
|
-
|
|
37
|
-
let total = rows[0]?.total ?? 0;
|
|
38
|
-
if (rows.length === 0 && options.pagination.offset > 0) {
|
|
39
|
-
const counted = await db
|
|
40
|
-
.select({ count: sql<number>`count(*)::int` })
|
|
41
|
-
.from(table as PgTable)
|
|
42
|
-
.where(options.where);
|
|
43
|
-
total = counted[0]?.count ?? 0;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
items: rows.map(({ total: _total, ...row }) => row as TTable['$inferSelect']),
|
|
48
|
-
total,
|
|
49
|
-
limit: options.pagination.limit,
|
|
50
|
-
offset: options.pagination.offset,
|
|
51
|
-
};
|
|
52
|
-
}
|