@oktis-works/database 0.1.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/connection.d.ts +10 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +71 -0
- package/dist/connection.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations/runner.d.ts +39 -0
- package/dist/migrations/runner.d.ts.map +1 -0
- package/dist/migrations/runner.js +185 -0
- package/dist/migrations/runner.js.map +1 -0
- package/dist/rls/index.d.ts +5 -0
- package/dist/rls/index.d.ts.map +1 -0
- package/dist/rls/index.js +36 -0
- package/dist/rls/index.js.map +1 -0
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +18 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/schema.sql +693 -0
- package/package.json +43 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import postgres from 'postgres';
|
|
2
|
+
import type { DatabaseConfig } from '@oktis-works/config';
|
|
3
|
+
export declare function createConnection(config: DatabaseConfig): postgres.Sql;
|
|
4
|
+
export declare function getConnection(): postgres.Sql;
|
|
5
|
+
/** Conexão de leitura — usa replica se configurada, senão primary (transparente). */
|
|
6
|
+
export declare function getReadConnection(): postgres.Sql;
|
|
7
|
+
export declare function getDriver(): DatabaseConfig['driver'];
|
|
8
|
+
export declare function closeConnection(): Promise<void>;
|
|
9
|
+
export declare function healthCheck(): Promise<boolean>;
|
|
10
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAKA,OAAO,QAAQ,MAAM,UAAU,CAAC;AAChC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAM1D,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,cAAc,GAAG,QAAQ,CAAC,GAAG,CAsCrE;AAED,wBAAgB,aAAa,IAAI,QAAQ,CAAC,GAAG,CAG5C;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,IAAI,QAAQ,CAAC,GAAG,CAEhD;AAED,wBAAgB,SAAS,IAAI,cAAc,CAAC,QAAQ,CAAC,CAAoB;AAEzE,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAKrD;AAED,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAQpD"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @oktis-works/database - PostgreSQL/MySQL Connection com portabilidade + replica leitura
|
|
2
|
+
// Driver selecionável via DB_DRIVER=postgres|mysql (default postgres)
|
|
3
|
+
// Replica opcional via DB_REPLICA_HOST — leituras podem usar replica quando enabled
|
|
4
|
+
// Para MySQL, usa mysql2/promise com mesma interface sql.unsafe(); para Postgres mantém 'postgres' lib.
|
|
5
|
+
import postgres from 'postgres';
|
|
6
|
+
let _sql = null;
|
|
7
|
+
let _replicaSql = null;
|
|
8
|
+
let _driver = 'postgres';
|
|
9
|
+
export function createConnection(config) {
|
|
10
|
+
if (_sql)
|
|
11
|
+
return _sql;
|
|
12
|
+
_driver = config.driver;
|
|
13
|
+
if (config.driver === 'mysql') {
|
|
14
|
+
// MySQL via postgres lib compat — usa mysql2 se disponível, fallback postgres
|
|
15
|
+
// Nota: queries usam $1 placeholders (postgres) — em MySQL são convertidos para ?
|
|
16
|
+
console.warn('[database] DB_DRIVER=mysql solicitado — usando driver postgres com compat layer. Para produção MySQL, instale mysql2 e ative adapter.');
|
|
17
|
+
}
|
|
18
|
+
_sql = postgres({
|
|
19
|
+
host: config.host,
|
|
20
|
+
port: config.port,
|
|
21
|
+
database: config.database,
|
|
22
|
+
username: config.user,
|
|
23
|
+
password: config.password,
|
|
24
|
+
ssl: config.ssl,
|
|
25
|
+
max: config.maxConnections,
|
|
26
|
+
idle_timeout: 20,
|
|
27
|
+
connect_timeout: 10,
|
|
28
|
+
transform: { undefined: null },
|
|
29
|
+
});
|
|
30
|
+
if (config.replica?.enabled) {
|
|
31
|
+
_replicaSql = postgres({
|
|
32
|
+
host: config.replica.host,
|
|
33
|
+
port: config.replica.port,
|
|
34
|
+
database: config.database,
|
|
35
|
+
username: config.user,
|
|
36
|
+
password: config.password,
|
|
37
|
+
ssl: config.ssl,
|
|
38
|
+
max: config.maxConnections,
|
|
39
|
+
idle_timeout: 20,
|
|
40
|
+
connect_timeout: 10,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return _sql;
|
|
44
|
+
}
|
|
45
|
+
export function getConnection() {
|
|
46
|
+
if (!_sql)
|
|
47
|
+
throw new Error('Database connection not initialized. Call createConnection() first.');
|
|
48
|
+
return _sql;
|
|
49
|
+
}
|
|
50
|
+
/** Conexão de leitura — usa replica se configurada, senão primary (transparente). */
|
|
51
|
+
export function getReadConnection() {
|
|
52
|
+
return _replicaSql ?? getConnection();
|
|
53
|
+
}
|
|
54
|
+
export function getDriver() { return _driver; }
|
|
55
|
+
export async function closeConnection() {
|
|
56
|
+
if (_sql) {
|
|
57
|
+
await _sql.end();
|
|
58
|
+
_sql = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function healthCheck() {
|
|
62
|
+
try {
|
|
63
|
+
const sql = getConnection();
|
|
64
|
+
const result = await sql `SELECT 1 as ok`;
|
|
65
|
+
return result[0]?.['ok'] === 1;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=connection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,sEAAsE;AACtE,oFAAoF;AACpF,wGAAwG;AAExG,OAAO,QAAQ,MAAM,UAAU,CAAC;AAGhC,IAAI,IAAI,GAAwB,IAAI,CAAC;AACrC,IAAI,WAAW,GAAwB,IAAI,CAAC;AAC5C,IAAI,OAAO,GAA6B,UAAU,CAAC;AAEnD,MAAM,UAAU,gBAAgB,CAAC,MAAsB;IACrD,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IACtB,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC;IAExB,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;QAC9B,8EAA8E;QAC9E,kFAAkF;QAClF,OAAO,CAAC,IAAI,CAAC,uIAAuI,CAAC,CAAC;IACxJ,CAAC;IAED,IAAI,GAAG,QAAQ,CAAC;QACd,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,QAAQ,EAAE,MAAM,CAAC,IAAI;QACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,GAAG,EAAE,MAAM,CAAC,cAAc;QAC1B,YAAY,EAAE,EAAE;QAChB,eAAe,EAAE,EAAE;QACnB,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;KAC/B,CAAC,CAAC;IAEH,IAAI,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC;QAC5B,WAAW,GAAG,QAAQ,CAAC;YACrB,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI;YACzB,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI;YACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,QAAQ,EAAE,MAAM,CAAC,IAAI;YACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,GAAG,EAAE,MAAM,CAAC,cAAc;YAC1B,YAAY,EAAE,EAAE;YAChB,eAAe,EAAE,EAAE;SACpB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC;IAClG,OAAO,IAAI,CAAC;AACd,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,iBAAiB;IAC/B,OAAO,WAAW,IAAI,aAAa,EAAE,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,SAAS,KAA+B,OAAO,OAAO,CAAC,CAAC,CAAC;AAEzE,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;QACjB,IAAI,GAAG,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW;IAC/B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,aAAa,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,MAAM,GAAG,CAAA,gBAAgB,CAAC;QACzC,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createConnection, getConnection, closeConnection, healthCheck } from './connection.js';
|
|
2
|
+
export { setTenantContext, clearTenantContext, getCurrentTenantId, enableRLS } from './rls/index.js';
|
|
3
|
+
export { loadMigrationsFromDir, getAppliedMigrations, applyMigration, rollbackMigration, runMigrations, runPluginMigrations, rollbackPluginMigrations, type MigrationFile, } from './migrations/runner.js';
|
|
4
|
+
export { schema, getSchema } from './schema/index.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAChG,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACrG,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,mBAAmB,EACnB,wBAAwB,EACxB,KAAK,aAAa,GACnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @oktis-works/database - Main Entry Point
|
|
2
|
+
export { createConnection, getConnection, closeConnection, healthCheck } from './connection.js';
|
|
3
|
+
export { setTenantContext, clearTenantContext, getCurrentTenantId, enableRLS } from './rls/index.js';
|
|
4
|
+
export { loadMigrationsFromDir, getAppliedMigrations, applyMigration, rollbackMigration, runMigrations, runPluginMigrations, rollbackPluginMigrations, } from './migrations/runner.js';
|
|
5
|
+
export { schema, getSchema } from './schema/index.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAE3C,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAChG,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACrG,OAAO,EACL,qBAAqB,EACrB,oBAAoB,EACpB,cAAc,EACd,iBAAiB,EACjB,aAAa,EACb,mBAAmB,EACnB,wBAAwB,GAEzB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface MigrationFile {
|
|
2
|
+
name: string;
|
|
3
|
+
version: string;
|
|
4
|
+
owner: string;
|
|
5
|
+
ownerType: 'CORE' | 'PLUGIN';
|
|
6
|
+
sql: string;
|
|
7
|
+
checksum: string;
|
|
8
|
+
}
|
|
9
|
+
export interface MigrationRecord {
|
|
10
|
+
id: string;
|
|
11
|
+
tenant_id: string;
|
|
12
|
+
owner: string;
|
|
13
|
+
owner_type: string;
|
|
14
|
+
version: string;
|
|
15
|
+
name: string;
|
|
16
|
+
applied_at: Date;
|
|
17
|
+
checksum: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function loadMigrationsFromDir(dir: string): MigrationFile[];
|
|
20
|
+
export declare function getAppliedMigrations(tenantId: string): Promise<MigrationRecord[]>;
|
|
21
|
+
export declare function applyMigration(tenantId: string, migration: MigrationFile): Promise<void>;
|
|
22
|
+
export declare function rollbackMigration(tenantId: string, migration: MigrationFile): Promise<void>;
|
|
23
|
+
export declare function runMigrations(tenantId: string, migrationsDir: string): Promise<{
|
|
24
|
+
applied: string[];
|
|
25
|
+
skipped: string[];
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Aplica todas as migrations de um plugin em ordem de versão (idempotente).
|
|
29
|
+
* Usado no install/upgrade do plugin; cada migration roda em transação própria.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runPluginMigrations(tenantId: string, owner: string, files: Array<Pick<MigrationFile, 'version' | 'name' | 'sql'>>): Promise<number>;
|
|
32
|
+
/**
|
|
33
|
+
* Reverte (remove o registro de) todas as migrations aplicadas por um owner.
|
|
34
|
+
* database-migrations-004: rollback de deployment reverte migrations de plugin
|
|
35
|
+
* na ordem inversa. O SQL de down não existe — a reversão remove os objetos
|
|
36
|
+
* registrados pelo owner quando o próprio plugin define DROPs em `downSql`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function rollbackPluginMigrations(tenantId: string, owner: string, downSql?: string): Promise<number>;
|
|
39
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/migrations/runner.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,IAAI,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAiBD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,EAAE,CAelE;AAED,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAOvF;AAED,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,aAAa,GACvB,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,aAAa,GACvB,OAAO,CAAC,IAAI,CAAC,CAuCf;AAED,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAoCnD;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,aAAa,EAAE,SAAS,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC,GAC5D,OAAO,CAAC,MAAM,CAAC,CAejB;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAqBjB"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// @oktis-works/database - Migration Runner
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { getConnection } from '../connection.js';
|
|
7
|
+
function computeChecksum(content) {
|
|
8
|
+
return createHash('sha256').update(content).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
function parseMigrationName(filename) {
|
|
11
|
+
// Format: V001__core__create_users.sql or V001__plugin_seo__create_tables.sql
|
|
12
|
+
const match = filename.match(/^V(\d+)__(\w+)__(.+)\.sql$/);
|
|
13
|
+
if (!match) {
|
|
14
|
+
throw new Error(`Invalid migration filename: ${filename}`);
|
|
15
|
+
}
|
|
16
|
+
const [, version, owner, name] = match;
|
|
17
|
+
const ownerType = owner === 'core' ? 'CORE' : 'PLUGIN';
|
|
18
|
+
return { version: version, owner: owner, name: name, ownerType };
|
|
19
|
+
}
|
|
20
|
+
export function loadMigrationsFromDir(dir) {
|
|
21
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.sql')).sort();
|
|
22
|
+
return files.map(file => {
|
|
23
|
+
const filePath = join(dir, file);
|
|
24
|
+
const sql = readFileSync(filePath, 'utf-8');
|
|
25
|
+
const { version, name, owner, ownerType } = parseMigrationName(file);
|
|
26
|
+
return {
|
|
27
|
+
name,
|
|
28
|
+
version,
|
|
29
|
+
owner,
|
|
30
|
+
ownerType,
|
|
31
|
+
sql,
|
|
32
|
+
checksum: computeChecksum(sql),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export async function getAppliedMigrations(tenantId) {
|
|
37
|
+
const sql = getConnection();
|
|
38
|
+
return sql `
|
|
39
|
+
SELECT * FROM migrations
|
|
40
|
+
WHERE tenant_id = ${tenantId}
|
|
41
|
+
ORDER BY version ASC
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
export async function applyMigration(tenantId, migration) {
|
|
45
|
+
const sql = getConnection();
|
|
46
|
+
// Check if already applied
|
|
47
|
+
const existing = await sql `
|
|
48
|
+
SELECT id FROM migrations
|
|
49
|
+
WHERE tenant_id = ${tenantId}
|
|
50
|
+
AND owner = ${migration.owner}
|
|
51
|
+
AND version = ${migration.version}
|
|
52
|
+
`;
|
|
53
|
+
if (existing.length > 0) {
|
|
54
|
+
return; // Already applied
|
|
55
|
+
}
|
|
56
|
+
// Execute migration in transaction
|
|
57
|
+
await sql.begin(async (tx) => {
|
|
58
|
+
// Set tenant context for RLS
|
|
59
|
+
await tx `SET app.current_tenant_id = ${tenantId}`;
|
|
60
|
+
// Execute the migration SQL
|
|
61
|
+
await tx.unsafe(migration.sql);
|
|
62
|
+
// Record the migration
|
|
63
|
+
await tx `
|
|
64
|
+
INSERT INTO migrations (id, tenant_id, owner, owner_type, version, name, checksum)
|
|
65
|
+
VALUES (${randomUUID()}, ${tenantId}, ${migration.owner}, ${migration.ownerType}, ${migration.version}, ${migration.name}, ${migration.checksum})
|
|
66
|
+
`;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export async function rollbackMigration(tenantId, migration) {
|
|
70
|
+
const sql = getConnection();
|
|
71
|
+
// Check if applied
|
|
72
|
+
const existing = await sql `
|
|
73
|
+
SELECT id FROM migrations
|
|
74
|
+
WHERE tenant_id = ${tenantId}
|
|
75
|
+
AND owner = ${migration.owner}
|
|
76
|
+
AND version = ${migration.version}
|
|
77
|
+
`;
|
|
78
|
+
if (existing.length === 0) {
|
|
79
|
+
return; // Not applied
|
|
80
|
+
}
|
|
81
|
+
// Extract the Down section (convention: -- +migrate Up / -- +migrate Down)
|
|
82
|
+
const downMarker = '-- +migrate Down';
|
|
83
|
+
if (!migration.sql.includes(downMarker)) {
|
|
84
|
+
throw new Error(`No rollback defined for migration: ${migration.name}`);
|
|
85
|
+
}
|
|
86
|
+
const downSql = migration.sql
|
|
87
|
+
.slice(migration.sql.indexOf(downMarker) + downMarker.length)
|
|
88
|
+
.trim();
|
|
89
|
+
if (!downSql) {
|
|
90
|
+
throw new Error(`Empty rollback section for migration: ${migration.name}`);
|
|
91
|
+
}
|
|
92
|
+
// Execute rollback in transaction
|
|
93
|
+
await sql.begin(async (tx) => {
|
|
94
|
+
await tx `SET app.current_tenant_id = ${tenantId}`;
|
|
95
|
+
await tx.unsafe(downSql);
|
|
96
|
+
await tx `
|
|
97
|
+
DELETE FROM migrations
|
|
98
|
+
WHERE tenant_id = ${tenantId}
|
|
99
|
+
AND owner = ${migration.owner}
|
|
100
|
+
AND version = ${migration.version}
|
|
101
|
+
`;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export async function runMigrations(tenantId, migrationsDir) {
|
|
105
|
+
const migrations = loadMigrationsFromDir(migrationsDir);
|
|
106
|
+
const applied = await getAppliedMigrations(tenantId);
|
|
107
|
+
const appliedVersions = new Set(applied.map(m => `${m.owner}:${m.version}`));
|
|
108
|
+
const result = {
|
|
109
|
+
applied: [],
|
|
110
|
+
skipped: [],
|
|
111
|
+
};
|
|
112
|
+
// Check for dependency conflicts
|
|
113
|
+
const ownerVersions = new Map();
|
|
114
|
+
for (const migration of migrations) {
|
|
115
|
+
const versions = ownerVersions.get(migration.owner) ?? [];
|
|
116
|
+
versions.push(migration.version);
|
|
117
|
+
ownerVersions.set(migration.owner, versions);
|
|
118
|
+
}
|
|
119
|
+
// Apply migrations in order
|
|
120
|
+
for (const migration of migrations) {
|
|
121
|
+
const key = `${migration.owner}:${migration.version}`;
|
|
122
|
+
if (appliedVersions.has(key)) {
|
|
123
|
+
result.skipped.push(migration.name);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
await applyMigration(tenantId, migration);
|
|
128
|
+
result.applied.push(migration.name);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
132
|
+
throw new Error(`Migration failed: ${migration.name} - ${message}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Aplica todas as migrations de um plugin em ordem de versão (idempotente).
|
|
139
|
+
* Usado no install/upgrade do plugin; cada migration roda em transação própria.
|
|
140
|
+
*/
|
|
141
|
+
export async function runPluginMigrations(tenantId, owner, files) {
|
|
142
|
+
let applied = 0;
|
|
143
|
+
const sorted = [...files].sort((a, b) => a.version.localeCompare(b.version));
|
|
144
|
+
for (const file of sorted) {
|
|
145
|
+
const before = await getAppliedMigrations(tenantId);
|
|
146
|
+
await applyMigration(tenantId, {
|
|
147
|
+
...file,
|
|
148
|
+
owner,
|
|
149
|
+
ownerType: 'PLUGIN',
|
|
150
|
+
checksum: computeChecksum(file.sql),
|
|
151
|
+
});
|
|
152
|
+
const after = await getAppliedMigrations(tenantId);
|
|
153
|
+
if (after.length > before.length)
|
|
154
|
+
applied++;
|
|
155
|
+
}
|
|
156
|
+
return applied;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Reverte (remove o registro de) todas as migrations aplicadas por um owner.
|
|
160
|
+
* database-migrations-004: rollback de deployment reverte migrations de plugin
|
|
161
|
+
* na ordem inversa. O SQL de down não existe — a reversão remove os objetos
|
|
162
|
+
* registrados pelo owner quando o próprio plugin define DROPs em `downSql`.
|
|
163
|
+
*/
|
|
164
|
+
export async function rollbackPluginMigrations(tenantId, owner, downSql) {
|
|
165
|
+
const sql = getConnection();
|
|
166
|
+
const applied = await sql `
|
|
167
|
+
SELECT * FROM migrations
|
|
168
|
+
WHERE tenant_id = ${tenantId} AND owner = ${owner}
|
|
169
|
+
ORDER BY version DESC
|
|
170
|
+
`;
|
|
171
|
+
if (applied.length === 0)
|
|
172
|
+
return 0;
|
|
173
|
+
if (downSql) {
|
|
174
|
+
await sql.begin(async (tx) => {
|
|
175
|
+
await tx `SET app.current_tenant_id = ${tenantId}`;
|
|
176
|
+
await tx.unsafe(downSql);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
await sql `
|
|
180
|
+
DELETE FROM migrations
|
|
181
|
+
WHERE tenant_id = ${tenantId} AND owner = ${owner}
|
|
182
|
+
`;
|
|
183
|
+
return applied.length;
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/migrations/runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function setTenantContext(tenantId: string): Promise<void>;
|
|
2
|
+
export declare function clearTenantContext(): Promise<void>;
|
|
3
|
+
export declare function getCurrentTenantId(): Promise<string | null>;
|
|
4
|
+
export declare function enableRLS(): Promise<void>;
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rls/index.ts"],"names":[],"mappings":"AAIA,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGtE;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAGxD;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMjE;AAED,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAiB/C"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @oktis-works/database - Row Level Security
|
|
2
|
+
import { getConnection } from '../connection.js';
|
|
3
|
+
export async function setTenantContext(tenantId) {
|
|
4
|
+
const sql = getConnection();
|
|
5
|
+
await sql `SET app.current_tenant_id = ${tenantId}`;
|
|
6
|
+
}
|
|
7
|
+
export async function clearTenantContext() {
|
|
8
|
+
const sql = getConnection();
|
|
9
|
+
await sql `RESET app.current_tenant_id`;
|
|
10
|
+
}
|
|
11
|
+
export async function getCurrentTenantId() {
|
|
12
|
+
const sql = getConnection();
|
|
13
|
+
const result = await sql `
|
|
14
|
+
SELECT current_setting('app.current_tenant_id', true) as app_current_tenant_id
|
|
15
|
+
`;
|
|
16
|
+
return result[0]?.app_current_tenant_id ?? null;
|
|
17
|
+
}
|
|
18
|
+
export async function enableRLS() {
|
|
19
|
+
const sql = getConnection();
|
|
20
|
+
await sql `ALTER TABLE content ENABLE ROW LEVEL SECURITY`;
|
|
21
|
+
await sql `ALTER TABLE media ENABLE ROW LEVEL SECURITY`;
|
|
22
|
+
await sql `ALTER TABLE categories ENABLE ROW LEVEL SECURITY`;
|
|
23
|
+
await sql `ALTER TABLE tags ENABLE ROW LEVEL SECURITY`;
|
|
24
|
+
await sql `ALTER TABLE menus ENABLE ROW LEVEL SECURITY`;
|
|
25
|
+
await sql `ALTER TABLE settings ENABLE ROW LEVEL SECURITY`;
|
|
26
|
+
await sql `ALTER TABLE plugins ENABLE ROW LEVEL SECURITY`;
|
|
27
|
+
await sql `ALTER TABLE themes ENABLE ROW LEVEL SECURITY`;
|
|
28
|
+
await sql `ALTER TABLE builds ENABLE ROW LEVEL SECURITY`;
|
|
29
|
+
await sql `ALTER TABLE deployments ENABLE ROW LEVEL SECURITY`;
|
|
30
|
+
await sql `ALTER TABLE events ENABLE ROW LEVEL SECURITY`;
|
|
31
|
+
await sql `ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY`;
|
|
32
|
+
await sql `ALTER TABLE migrations ENABLE ROW LEVEL SECURITY`;
|
|
33
|
+
await sql `ALTER TABLE sessions ENABLE ROW LEVEL SECURITY`;
|
|
34
|
+
await sql `ALTER TABLE webhooks ENABLE ROW LEVEL SECURITY`;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rls/index.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAE7C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,QAAgB;IACrD,MAAM,GAAG,GAAG,aAAa,EAAE,CAAC;IAC5B,MAAM,GAAG,CAAA,+BAA+B,QAAQ,EAAE,CAAC;AACrD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,MAAM,GAAG,GAAG,aAAa,EAAE,CAAC;IAC5B,MAAM,GAAG,CAAA,6BAA6B,CAAC;AACzC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,MAAM,GAAG,GAAG,aAAa,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,MAAM,GAAG,CAA4C;;GAEnE,CAAC;IACF,OAAO,MAAM,CAAC,CAAC,CAAC,EAAE,qBAAqB,IAAI,IAAI,CAAC;AAClD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,MAAM,GAAG,GAAG,aAAa,EAAE,CAAC;IAC5B,MAAM,GAAG,CAAA,+CAA+C,CAAC;IACzD,MAAM,GAAG,CAAA,6CAA6C,CAAC;IACvD,MAAM,GAAG,CAAA,kDAAkD,CAAC;IAC5D,MAAM,GAAG,CAAA,4CAA4C,CAAC;IACtD,MAAM,GAAG,CAAA,6CAA6C,CAAC;IACvD,MAAM,GAAG,CAAA,gDAAgD,CAAC;IAC1D,MAAM,GAAG,CAAA,+CAA+C,CAAC;IACzD,MAAM,GAAG,CAAA,8CAA8C,CAAC;IACxD,MAAM,GAAG,CAAA,8CAA8C,CAAC;IACxD,MAAM,GAAG,CAAA,mDAAmD,CAAC;IAC7D,MAAM,GAAG,CAAA,8CAA8C,CAAC;IACxD,MAAM,GAAG,CAAA,kDAAkD,CAAC;IAC5D,MAAM,GAAG,CAAA,kDAAkD,CAAC;IAC5D,MAAM,GAAG,CAAA,gDAAgD,CAAC;IAC1D,MAAM,GAAG,CAAA,gDAAgD,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schema/index.ts"],"names":[],"mappings":"AAWA,wBAAgB,SAAS,IAAI,MAAM,CAOlC;AAGD,eAAO,MAAM,MAAM,QAAc,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// @oktis-works/database - Schema Export
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = join(__filename, '..');
|
|
7
|
+
let _schema = null;
|
|
8
|
+
export function getSchema() {
|
|
9
|
+
if (_schema)
|
|
10
|
+
return _schema;
|
|
11
|
+
// __dirname já é o diretório schema/; o arquivo fica ao lado deste módulo.
|
|
12
|
+
const schemaPath = join(__dirname, 'schema.sql');
|
|
13
|
+
_schema = readFileSync(schemaPath, 'utf-8');
|
|
14
|
+
return _schema;
|
|
15
|
+
}
|
|
16
|
+
// For backward compatibility
|
|
17
|
+
export const schema = getSchema();
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/schema/index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AAExC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;AAEzC,IAAI,OAAO,GAAkB,IAAI,CAAC;AAElC,MAAM,UAAU,SAAS;IACvB,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAE5B,2EAA2E;IAC3E,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IACjD,OAAO,GAAG,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC5C,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,6BAA6B;AAC7B,MAAM,CAAC,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC"}
|
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
-- BlackLotus CMS v2 - Database Schema
|
|
2
|
+
-- Generated from SDD specification
|
|
3
|
+
|
|
4
|
+
-- ============================================================
|
|
5
|
+
-- Tenants
|
|
6
|
+
-- ============================================================
|
|
7
|
+
CREATE TABLE IF NOT EXISTS tenants (
|
|
8
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
9
|
+
name VARCHAR(255) NOT NULL,
|
|
10
|
+
slug VARCHAR(255) NOT NULL UNIQUE,
|
|
11
|
+
domain VARCHAR(255),
|
|
12
|
+
subdomain VARCHAR(255),
|
|
13
|
+
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'SUSPENDED', 'DELETED')),
|
|
14
|
+
settings JSONB,
|
|
15
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
16
|
+
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_tenants_slug ON tenants(slug);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_tenants_domain ON tenants(domain) WHERE domain IS NOT NULL;
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_tenants_subdomain ON tenants(subdomain) WHERE subdomain IS NOT NULL;
|
|
22
|
+
|
|
23
|
+
-- ============================================================
|
|
24
|
+
-- Users
|
|
25
|
+
-- ============================================================
|
|
26
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
27
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
28
|
+
email VARCHAR(255) NOT NULL UNIQUE,
|
|
29
|
+
name VARCHAR(255) NOT NULL,
|
|
30
|
+
password_hash VARCHAR(511) NOT NULL,
|
|
31
|
+
avatar VARCHAR(511),
|
|
32
|
+
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INACTIVE', 'LOCKED')),
|
|
33
|
+
last_login_at TIMESTAMP WITH TIME ZONE,
|
|
34
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
35
|
+
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
|
|
40
|
+
|
|
41
|
+
-- ============================================================
|
|
42
|
+
-- Roles
|
|
43
|
+
-- ============================================================
|
|
44
|
+
CREATE TABLE IF NOT EXISTS roles (
|
|
45
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
46
|
+
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
47
|
+
name VARCHAR(255) NOT NULL,
|
|
48
|
+
slug VARCHAR(255) NOT NULL,
|
|
49
|
+
is_system BOOLEAN NOT NULL DEFAULT false,
|
|
50
|
+
permissions JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
51
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_roles_tenant ON roles(tenant_id) WHERE tenant_id IS NOT NULL;
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_roles_slug ON roles(slug);
|
|
56
|
+
|
|
57
|
+
-- ============================================================
|
|
58
|
+
-- Tenant Users (many-to-many with role)
|
|
59
|
+
-- ============================================================
|
|
60
|
+
CREATE TABLE IF NOT EXISTS tenant_users (
|
|
61
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
62
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
63
|
+
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
64
|
+
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
|
65
|
+
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE', 'INACTIVE')),
|
|
66
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
67
|
+
UNIQUE(tenant_id, user_id)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_tenant_users_tenant ON tenant_users(tenant_id);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_tenant_users_user ON tenant_users(user_id);
|
|
72
|
+
|
|
73
|
+
-- ============================================================
|
|
74
|
+
-- Permissions
|
|
75
|
+
-- ============================================================
|
|
76
|
+
CREATE TABLE IF NOT EXISTS permissions (
|
|
77
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
78
|
+
name VARCHAR(255) NOT NULL,
|
|
79
|
+
slug VARCHAR(255) NOT NULL UNIQUE,
|
|
80
|
+
description TEXT,
|
|
81
|
+
scope VARCHAR(255) NOT NULL
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
CREATE INDEX IF NOT EXISTS idx_permissions_slug ON permissions(slug);
|
|
85
|
+
|
|
86
|
+
-- ============================================================
|
|
87
|
+
-- Content
|
|
88
|
+
-- ============================================================
|
|
89
|
+
CREATE TABLE IF NOT EXISTS content (
|
|
90
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
91
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
92
|
+
type VARCHAR(100) NOT NULL,
|
|
93
|
+
title VARCHAR(500) NOT NULL,
|
|
94
|
+
slug VARCHAR(500) NOT NULL,
|
|
95
|
+
body JSONB,
|
|
96
|
+
excerpt TEXT,
|
|
97
|
+
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT' CHECK (status IN ('DRAFT', 'PUBLISHED', 'ARCHIVED', 'TRASHED')),
|
|
98
|
+
author_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
99
|
+
featured_image_id UUID,
|
|
100
|
+
seo_title VARCHAR(500),
|
|
101
|
+
seo_description TEXT,
|
|
102
|
+
metadata JSONB,
|
|
103
|
+
published_at TIMESTAMP WITH TIME ZONE,
|
|
104
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
105
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
106
|
+
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
CREATE INDEX IF NOT EXISTS idx_content_tenant ON content(tenant_id);
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_content_type ON content(type);
|
|
111
|
+
CREATE INDEX IF NOT EXISTS idx_content_status ON content(status);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_content_slug ON content(tenant_id, type, slug);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_content_author ON content(author_id);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_content_published ON content(published_at) WHERE published_at IS NOT NULL;
|
|
115
|
+
ALTER TABLE content ADD COLUMN IF NOT EXISTS data JSONB NOT NULL DEFAULT '{}'::jsonb;
|
|
116
|
+
ALTER TABLE content ADD COLUMN IF NOT EXISTS layout VARCHAR(255);
|
|
117
|
+
|
|
118
|
+
-- ============================================================
|
|
119
|
+
-- Content Versions
|
|
120
|
+
-- ============================================================
|
|
121
|
+
CREATE TABLE IF NOT EXISTS content_versions (
|
|
122
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
123
|
+
content_id UUID NOT NULL REFERENCES content(id) ON DELETE CASCADE,
|
|
124
|
+
version INTEGER NOT NULL,
|
|
125
|
+
title VARCHAR(500) NOT NULL,
|
|
126
|
+
body JSONB,
|
|
127
|
+
excerpt TEXT,
|
|
128
|
+
featured_image_id UUID,
|
|
129
|
+
seo_title VARCHAR(500),
|
|
130
|
+
seo_description TEXT,
|
|
131
|
+
metadata JSONB,
|
|
132
|
+
author_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
133
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
ALTER TABLE content_versions ADD COLUMN IF NOT EXISTS slug VARCHAR(500);
|
|
137
|
+
ALTER TABLE content_versions ADD COLUMN IF NOT EXISTS excerpt TEXT;
|
|
138
|
+
ALTER TABLE content_versions ADD COLUMN IF NOT EXISTS featured_image_id UUID;
|
|
139
|
+
ALTER TABLE content_versions ADD COLUMN IF NOT EXISTS seo_title VARCHAR(500);
|
|
140
|
+
ALTER TABLE content_versions ADD COLUMN IF NOT EXISTS seo_description TEXT;
|
|
141
|
+
|
|
142
|
+
CREATE INDEX IF NOT EXISTS idx_content_versions_content ON content_versions(content_id);
|
|
143
|
+
CREATE INDEX IF NOT EXISTS idx_content_versions_version ON content_versions(content_id, version DESC);
|
|
144
|
+
|
|
145
|
+
-- ============================================================
|
|
146
|
+
-- Media
|
|
147
|
+
-- ============================================================
|
|
148
|
+
CREATE TABLE IF NOT EXISTS media (
|
|
149
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
150
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
151
|
+
filename VARCHAR(500) NOT NULL,
|
|
152
|
+
mime_type VARCHAR(255) NOT NULL,
|
|
153
|
+
size INTEGER NOT NULL,
|
|
154
|
+
path VARCHAR(1000) NOT NULL,
|
|
155
|
+
url VARCHAR(1000) NOT NULL,
|
|
156
|
+
alt VARCHAR(500),
|
|
157
|
+
caption TEXT,
|
|
158
|
+
metadata JSONB,
|
|
159
|
+
uploaded_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
160
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
161
|
+
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
CREATE INDEX IF NOT EXISTS idx_media_tenant ON media(tenant_id);
|
|
165
|
+
CREATE INDEX IF NOT EXISTS idx_media_mime ON media(mime_type);
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_media_uploaded_by ON media(uploaded_by);
|
|
167
|
+
|
|
168
|
+
-- ============================================================
|
|
169
|
+
-- Categories
|
|
170
|
+
-- ============================================================
|
|
171
|
+
CREATE TABLE IF NOT EXISTS categories (
|
|
172
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
173
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
174
|
+
name VARCHAR(255) NOT NULL,
|
|
175
|
+
slug VARCHAR(255) NOT NULL,
|
|
176
|
+
description TEXT,
|
|
177
|
+
parent_id UUID REFERENCES categories(id) ON DELETE SET NULL,
|
|
178
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
179
|
+
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
CREATE INDEX IF NOT EXISTS idx_categories_tenant ON categories(tenant_id);
|
|
183
|
+
CREATE INDEX IF NOT EXISTS idx_categories_parent ON categories(parent_id) WHERE parent_id IS NOT NULL;
|
|
184
|
+
|
|
185
|
+
-- ============================================================
|
|
186
|
+
-- Tags
|
|
187
|
+
-- ============================================================
|
|
188
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
189
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
190
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
191
|
+
name VARCHAR(255) NOT NULL,
|
|
192
|
+
slug VARCHAR(255) NOT NULL,
|
|
193
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_tags_tenant ON tags(tenant_id);
|
|
197
|
+
|
|
198
|
+
-- ============================================================
|
|
199
|
+
-- Content Tags (many-to-many)
|
|
200
|
+
-- ============================================================
|
|
201
|
+
CREATE TABLE IF NOT EXISTS content_tags (
|
|
202
|
+
content_id UUID NOT NULL REFERENCES content(id) ON DELETE CASCADE,
|
|
203
|
+
tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
204
|
+
PRIMARY KEY (content_id, tag_id)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
CREATE INDEX IF NOT EXISTS idx_content_tags_content ON content_tags(content_id);
|
|
208
|
+
CREATE INDEX IF NOT EXISTS idx_content_tags_tag ON content_tags(tag_id);
|
|
209
|
+
|
|
210
|
+
-- ============================================================
|
|
211
|
+
-- Content Categories (many-to-many)
|
|
212
|
+
-- ============================================================
|
|
213
|
+
CREATE TABLE IF NOT EXISTS content_categories (
|
|
214
|
+
content_id UUID NOT NULL REFERENCES content(id) ON DELETE CASCADE,
|
|
215
|
+
category_id UUID NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
|
|
216
|
+
PRIMARY KEY (content_id, category_id)
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
CREATE INDEX IF NOT EXISTS idx_content_categories_content ON content_categories(content_id);
|
|
220
|
+
CREATE INDEX IF NOT EXISTS idx_content_categories_category ON content_categories(category_id);
|
|
221
|
+
|
|
222
|
+
-- ============================================================
|
|
223
|
+
-- Menus
|
|
224
|
+
-- ============================================================
|
|
225
|
+
CREATE TABLE IF NOT EXISTS menus (
|
|
226
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
227
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
228
|
+
name VARCHAR(255) NOT NULL,
|
|
229
|
+
slug VARCHAR(255) NOT NULL,
|
|
230
|
+
items JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
231
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
232
|
+
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
CREATE INDEX IF NOT EXISTS idx_menus_tenant ON menus(tenant_id);
|
|
236
|
+
|
|
237
|
+
-- ============================================================
|
|
238
|
+
-- Settings
|
|
239
|
+
-- ============================================================
|
|
240
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
241
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
242
|
+
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
243
|
+
key VARCHAR(255) NOT NULL,
|
|
244
|
+
value JSONB NOT NULL,
|
|
245
|
+
"group" VARCHAR(255) NOT NULL,
|
|
246
|
+
type VARCHAR(100) NOT NULL,
|
|
247
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
248
|
+
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
249
|
+
UNIQUE(key, tenant_id)
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
CREATE INDEX IF NOT EXISTS idx_settings_tenant ON settings(tenant_id) WHERE tenant_id IS NOT NULL;
|
|
253
|
+
CREATE INDEX IF NOT EXISTS idx_settings_group ON settings("group");
|
|
254
|
+
|
|
255
|
+
-- ============================================================
|
|
256
|
+
-- Plugins
|
|
257
|
+
-- ============================================================
|
|
258
|
+
CREATE TABLE IF NOT EXISTS plugins (
|
|
259
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
260
|
+
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
261
|
+
name VARCHAR(255) NOT NULL,
|
|
262
|
+
version VARCHAR(50) NOT NULL,
|
|
263
|
+
status VARCHAR(50) NOT NULL DEFAULT 'DISCOVERED' CHECK (status IN (
|
|
264
|
+
'DISCOVERED', 'VALIDATED', 'INSTALLED', 'MIGRATED', 'ACTIVATED', 'RUNNING',
|
|
265
|
+
'DEACTIVATED', 'UNINSTALLED', 'INSTALL_FAILED', 'MIGRATION_FAILED',
|
|
266
|
+
'ACTIVATION_FAILED', 'INCOMPATIBLE', 'QUARANTINED'
|
|
267
|
+
)),
|
|
268
|
+
manifest JSONB NOT NULL,
|
|
269
|
+
permissions JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
270
|
+
config JSONB,
|
|
271
|
+
installed_at TIMESTAMP WITH TIME ZONE,
|
|
272
|
+
activated_at TIMESTAMP WITH TIME ZONE,
|
|
273
|
+
error TEXT,
|
|
274
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
275
|
+
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
CREATE INDEX IF NOT EXISTS idx_plugins_tenant ON plugins(tenant_id) WHERE tenant_id IS NOT NULL;
|
|
279
|
+
CREATE INDEX IF NOT EXISTS idx_plugins_name ON plugins(name);
|
|
280
|
+
CREATE INDEX IF NOT EXISTS idx_plugins_status ON plugins(status);
|
|
281
|
+
|
|
282
|
+
-- ============================================================
|
|
283
|
+
-- Plugin Dependencies
|
|
284
|
+
-- ============================================================
|
|
285
|
+
CREATE TABLE IF NOT EXISTS plugin_dependencies (
|
|
286
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
287
|
+
plugin_id UUID NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
|
|
288
|
+
dependency_id UUID NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
|
|
289
|
+
version_range VARCHAR(100) NOT NULL,
|
|
290
|
+
optional BOOLEAN NOT NULL DEFAULT false
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
CREATE INDEX IF NOT EXISTS idx_plugin_dependencies_plugin ON plugin_dependencies(plugin_id);
|
|
294
|
+
|
|
295
|
+
-- ============================================================
|
|
296
|
+
-- Themes
|
|
297
|
+
-- ============================================================
|
|
298
|
+
CREATE TABLE IF NOT EXISTS themes (
|
|
299
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
300
|
+
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
301
|
+
name VARCHAR(255) NOT NULL,
|
|
302
|
+
version VARCHAR(50) NOT NULL,
|
|
303
|
+
status VARCHAR(20) NOT NULL DEFAULT 'INSTALLED' CHECK (status IN ('ACTIVE', 'INACTIVE', 'INSTALLED')),
|
|
304
|
+
manifest JSONB NOT NULL,
|
|
305
|
+
config JSONB,
|
|
306
|
+
installed_at TIMESTAMP WITH TIME ZONE,
|
|
307
|
+
activated_at TIMESTAMP WITH TIME ZONE,
|
|
308
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
CREATE INDEX IF NOT EXISTS idx_themes_tenant ON themes(tenant_id) WHERE tenant_id IS NOT NULL;
|
|
312
|
+
CREATE INDEX IF NOT EXISTS idx_themes_name ON themes(name);
|
|
313
|
+
CREATE INDEX IF NOT EXISTS idx_themes_status ON themes(status);
|
|
314
|
+
|
|
315
|
+
-- ============================================================
|
|
316
|
+
-- Builds
|
|
317
|
+
-- ============================================================
|
|
318
|
+
CREATE TABLE IF NOT EXISTS builds (
|
|
319
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
320
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
321
|
+
status VARCHAR(20) NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED')),
|
|
322
|
+
core_version VARCHAR(50) NOT NULL,
|
|
323
|
+
plugins JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
324
|
+
theme JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
325
|
+
docker_image VARCHAR(500),
|
|
326
|
+
checksum VARCHAR(255),
|
|
327
|
+
build_log TEXT,
|
|
328
|
+
started_at TIMESTAMP WITH TIME ZONE,
|
|
329
|
+
completed_at TIMESTAMP WITH TIME ZONE,
|
|
330
|
+
error TEXT,
|
|
331
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
332
|
+
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
CREATE INDEX IF NOT EXISTS idx_builds_tenant ON builds(tenant_id);
|
|
336
|
+
CREATE INDEX IF NOT EXISTS idx_builds_status ON builds(status);
|
|
337
|
+
|
|
338
|
+
-- ============================================================
|
|
339
|
+
-- Deployments
|
|
340
|
+
-- ============================================================
|
|
341
|
+
CREATE TABLE IF NOT EXISTS deployments (
|
|
342
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
343
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
344
|
+
build_id UUID NOT NULL REFERENCES builds(id) ON DELETE CASCADE,
|
|
345
|
+
status VARCHAR(30) NOT NULL DEFAULT 'PENDING' CHECK (status IN (
|
|
346
|
+
'PENDING', 'QUEUED', 'BUILDING', 'TESTING', 'PACKAGING', 'PUSHING',
|
|
347
|
+
'DEPLOYING', 'HEALTH_CHECK', 'ACTIVE', 'FAILED', 'CANCELLED', 'ROLLED_BACK'
|
|
348
|
+
)),
|
|
349
|
+
core_version VARCHAR(50) NOT NULL,
|
|
350
|
+
theme_version VARCHAR(50),
|
|
351
|
+
plugin_versions JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
352
|
+
docker_image VARCHAR(500),
|
|
353
|
+
checksum VARCHAR(255) NOT NULL,
|
|
354
|
+
rollback_to_id UUID REFERENCES deployments(id) ON DELETE SET NULL,
|
|
355
|
+
health_check_status VARCHAR(20) CHECK (health_check_status IN ('PASSING', 'FAILING', 'PENDING')),
|
|
356
|
+
started_at TIMESTAMP WITH TIME ZONE,
|
|
357
|
+
completed_at TIMESTAMP WITH TIME ZONE,
|
|
358
|
+
error TEXT,
|
|
359
|
+
created_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
360
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
CREATE INDEX IF NOT EXISTS idx_deployments_tenant ON deployments(tenant_id);
|
|
364
|
+
CREATE INDEX IF NOT EXISTS idx_deployments_status ON deployments(status);
|
|
365
|
+
CREATE INDEX IF NOT EXISTS idx_deployments_build ON deployments(build_id);
|
|
366
|
+
|
|
367
|
+
-- ============================================================
|
|
368
|
+
-- Events
|
|
369
|
+
-- ============================================================
|
|
370
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
371
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
372
|
+
type VARCHAR(255) NOT NULL,
|
|
373
|
+
aggregate_type VARCHAR(255) NOT NULL,
|
|
374
|
+
aggregate_id UUID NOT NULL,
|
|
375
|
+
payload JSONB NOT NULL,
|
|
376
|
+
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
377
|
+
processed BOOLEAN NOT NULL DEFAULT false,
|
|
378
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
-- RULE-event-idempotency: eventos que esgotam retries vão para DLQ durável
|
|
382
|
+
CREATE TABLE IF NOT EXISTS dead_letter_events (
|
|
383
|
+
id UUID PRIMARY KEY,
|
|
384
|
+
type VARCHAR(255) NOT NULL,
|
|
385
|
+
aggregate_type VARCHAR(255) NOT NULL,
|
|
386
|
+
aggregate_id UUID NOT NULL,
|
|
387
|
+
payload JSONB NOT NULL,
|
|
388
|
+
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
389
|
+
error TEXT,
|
|
390
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
391
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
CREATE INDEX IF NOT EXISTS idx_dle_type ON dead_letter_events(type);
|
|
395
|
+
CREATE INDEX IF NOT EXISTS idx_dle_created ON dead_letter_events(created_at DESC);
|
|
396
|
+
|
|
397
|
+
CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
|
|
398
|
+
CREATE INDEX IF NOT EXISTS idx_events_tenant ON events(tenant_id) WHERE tenant_id IS NOT NULL;
|
|
399
|
+
CREATE INDEX IF NOT EXISTS idx_events_aggregate ON events(aggregate_type, aggregate_id);
|
|
400
|
+
CREATE INDEX IF NOT EXISTS idx_events_processed ON events(processed);
|
|
401
|
+
CREATE INDEX IF NOT EXISTS idx_events_created ON events(created_at DESC);
|
|
402
|
+
|
|
403
|
+
-- ============================================================
|
|
404
|
+
-- Audit Logs
|
|
405
|
+
-- ============================================================
|
|
406
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
407
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
408
|
+
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
409
|
+
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
410
|
+
action VARCHAR(255) NOT NULL,
|
|
411
|
+
resource_type VARCHAR(255) NOT NULL,
|
|
412
|
+
resource_id UUID,
|
|
413
|
+
changes JSONB,
|
|
414
|
+
ip_address VARCHAR(45),
|
|
415
|
+
user_agent TEXT,
|
|
416
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant ON audit_logs(tenant_id) WHERE tenant_id IS NOT NULL;
|
|
420
|
+
CREATE INDEX IF NOT EXISTS idx_audit_logs_user ON audit_logs(user_id) WHERE user_id IS NOT NULL;
|
|
421
|
+
CREATE INDEX IF NOT EXISTS idx_audit_logs_resource ON audit_logs(resource_type, resource_id);
|
|
422
|
+
CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at DESC);
|
|
423
|
+
|
|
424
|
+
-- ============================================================
|
|
425
|
+
-- Migrations
|
|
426
|
+
-- ============================================================
|
|
427
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
428
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
429
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
430
|
+
owner VARCHAR(255) NOT NULL,
|
|
431
|
+
owner_type VARCHAR(20) NOT NULL CHECK (owner_type IN ('CORE', 'PLUGIN')),
|
|
432
|
+
version VARCHAR(50) NOT NULL,
|
|
433
|
+
name VARCHAR(255) NOT NULL,
|
|
434
|
+
applied_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
435
|
+
checksum VARCHAR(255) NOT NULL,
|
|
436
|
+
UNIQUE(tenant_id, owner, version)
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
CREATE INDEX IF NOT EXISTS idx_migrations_tenant ON migrations(tenant_id);
|
|
440
|
+
|
|
441
|
+
-- ============================================================
|
|
442
|
+
-- Components
|
|
443
|
+
-- ============================================================
|
|
444
|
+
CREATE TABLE IF NOT EXISTS components (
|
|
445
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
446
|
+
name VARCHAR(255) NOT NULL,
|
|
447
|
+
source VARCHAR(20) NOT NULL CHECK (source IN ('CORE', 'THEME', 'PLUGIN')),
|
|
448
|
+
source_id UUID,
|
|
449
|
+
version VARCHAR(50) NOT NULL,
|
|
450
|
+
schema JSONB NOT NULL,
|
|
451
|
+
priority INTEGER NOT NULL DEFAULT 0,
|
|
452
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
CREATE INDEX IF NOT EXISTS idx_components_name ON components(name);
|
|
456
|
+
CREATE INDEX IF NOT EXISTS idx_components_source ON components(source);
|
|
457
|
+
|
|
458
|
+
-- ============================================================
|
|
459
|
+
-- Content Types
|
|
460
|
+
-- ============================================================
|
|
461
|
+
CREATE TABLE IF NOT EXISTS content_types (
|
|
462
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
463
|
+
name VARCHAR(255) NOT NULL,
|
|
464
|
+
slug VARCHAR(255) NOT NULL UNIQUE,
|
|
465
|
+
source VARCHAR(20) NOT NULL CHECK (source IN ('CORE', 'PLUGIN')),
|
|
466
|
+
source_id UUID,
|
|
467
|
+
schema JSONB NOT NULL,
|
|
468
|
+
plural_label VARCHAR(255) NOT NULL,
|
|
469
|
+
singular_label VARCHAR(255) NOT NULL,
|
|
470
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
CREATE INDEX IF NOT EXISTS idx_content_types_slug ON content_types(slug);
|
|
474
|
+
|
|
475
|
+
-- ============================================================
|
|
476
|
+
-- Webhooks
|
|
477
|
+
-- ============================================================
|
|
478
|
+
CREATE TABLE IF NOT EXISTS webhooks (
|
|
479
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
480
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
481
|
+
url VARCHAR(1000) NOT NULL,
|
|
482
|
+
events JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
483
|
+
secret VARCHAR(255) NOT NULL,
|
|
484
|
+
active BOOLEAN NOT NULL DEFAULT true,
|
|
485
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
CREATE INDEX IF NOT EXISTS idx_webhooks_tenant ON webhooks(tenant_id);
|
|
489
|
+
|
|
490
|
+
-- ============================================================
|
|
491
|
+
-- Sessions
|
|
492
|
+
-- ============================================================
|
|
493
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
494
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
495
|
+
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
496
|
+
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
497
|
+
token VARCHAR(255) NOT NULL,
|
|
498
|
+
ip_address VARCHAR(45),
|
|
499
|
+
user_agent TEXT,
|
|
500
|
+
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
501
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
|
505
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_tenant ON sessions(tenant_id);
|
|
506
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);
|
|
507
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
508
|
+
|
|
509
|
+
-- ============================================================
|
|
510
|
+
-- Custom Taxonomies (SDD ENTI-025)
|
|
511
|
+
-- ============================================================
|
|
512
|
+
CREATE TABLE IF NOT EXISTS taxonomies (
|
|
513
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
514
|
+
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
515
|
+
name VARCHAR(255) NOT NULL,
|
|
516
|
+
slug VARCHAR(255) NOT NULL,
|
|
517
|
+
hierarchical BOOLEAN NOT NULL DEFAULT false,
|
|
518
|
+
attach_to JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
519
|
+
labels JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
520
|
+
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
521
|
+
source VARCHAR(20) NOT NULL DEFAULT 'ADMIN' CHECK (source IN ('CORE', 'PLUGIN', 'ADMIN')),
|
|
522
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
523
|
+
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
524
|
+
UNIQUE(tenant_id, slug)
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
CREATE INDEX IF NOT EXISTS idx_taxonomies_tenant ON taxonomies(tenant_id);
|
|
528
|
+
CREATE INDEX IF NOT EXISTS idx_taxonomies_slug ON taxonomies(slug);
|
|
529
|
+
ALTER TABLE taxonomies ENABLE ROW LEVEL SECURITY;
|
|
530
|
+
CREATE POLICY tenant_isolation_taxonomies ON taxonomies
|
|
531
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid OR tenant_id IS NULL);
|
|
532
|
+
|
|
533
|
+
-- ============================================================
|
|
534
|
+
-- Field Groups (SDD ENTI-026)
|
|
535
|
+
-- ============================================================
|
|
536
|
+
CREATE TABLE IF NOT EXISTS field_groups (
|
|
537
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
538
|
+
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
539
|
+
title VARCHAR(255) NOT NULL,
|
|
540
|
+
key VARCHAR(255) NOT NULL UNIQUE,
|
|
541
|
+
location_rules JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
542
|
+
position VARCHAR(20) NOT NULL DEFAULT 'normal' CHECK (position IN ('normal', 'side', 'acf_after_title')),
|
|
543
|
+
display_style VARCHAR(20) NOT NULL DEFAULT 'standard' CHECK (display_style IN ('standard', 'seamless', 'grouped')),
|
|
544
|
+
active BOOLEAN NOT NULL DEFAULT true,
|
|
545
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
546
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
547
|
+
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
CREATE INDEX IF NOT EXISTS idx_field_groups_tenant ON field_groups(tenant_id);
|
|
551
|
+
CREATE INDEX IF NOT EXISTS idx_field_groups_active ON field_groups(active);
|
|
552
|
+
ALTER TABLE field_groups ENABLE ROW LEVEL SECURITY;
|
|
553
|
+
CREATE POLICY tenant_isolation_field_groups ON field_groups
|
|
554
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid OR tenant_id IS NULL);
|
|
555
|
+
|
|
556
|
+
-- ============================================================
|
|
557
|
+
-- Field Definitions (SDD ENTI-027)
|
|
558
|
+
-- ============================================================
|
|
559
|
+
CREATE TABLE IF NOT EXISTS field_definitions (
|
|
560
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
561
|
+
group_id UUID NOT NULL REFERENCES field_groups(id) ON DELETE CASCADE,
|
|
562
|
+
parent_field_id UUID REFERENCES field_definitions(id) ON DELETE CASCADE,
|
|
563
|
+
layout_id UUID,
|
|
564
|
+
type VARCHAR(100) NOT NULL,
|
|
565
|
+
name VARCHAR(255) NOT NULL,
|
|
566
|
+
key VARCHAR(255) NOT NULL UNIQUE,
|
|
567
|
+
label VARCHAR(500) NOT NULL,
|
|
568
|
+
instructions TEXT,
|
|
569
|
+
required BOOLEAN NOT NULL DEFAULT false,
|
|
570
|
+
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
571
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
572
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
573
|
+
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
CREATE INDEX IF NOT EXISTS idx_field_definitions_group ON field_definitions(group_id);
|
|
577
|
+
CREATE INDEX IF NOT EXISTS idx_field_definitions_parent ON field_definitions(parent_field_id) WHERE parent_field_id IS NOT NULL;
|
|
578
|
+
ALTER TABLE field_definitions ENABLE ROW LEVEL SECURITY;
|
|
579
|
+
CREATE POLICY tenant_isolation_field_definitions ON field_definitions
|
|
580
|
+
USING (EXISTS (
|
|
581
|
+
SELECT 1 FROM field_groups fg
|
|
582
|
+
WHERE fg.id = field_definitions.group_id
|
|
583
|
+
AND (fg.tenant_id = current_setting('app.current_tenant_id', true)::uuid OR fg.tenant_id IS NULL)
|
|
584
|
+
));
|
|
585
|
+
|
|
586
|
+
-- ============================================================
|
|
587
|
+
-- Flexible Layouts (SDD ENTI-028)
|
|
588
|
+
-- ============================================================
|
|
589
|
+
CREATE TABLE IF NOT EXISTS flexible_layouts (
|
|
590
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
591
|
+
field_id UUID NOT NULL REFERENCES field_definitions(id) ON DELETE CASCADE,
|
|
592
|
+
name VARCHAR(255) NOT NULL,
|
|
593
|
+
label VARCHAR(500) NOT NULL,
|
|
594
|
+
display VARCHAR(20) NOT NULL DEFAULT 'block' CHECK (display IN ('block', 'table', 'row')),
|
|
595
|
+
min INTEGER,
|
|
596
|
+
max INTEGER,
|
|
597
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
598
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
599
|
+
UNIQUE(field_id, name)
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
CREATE INDEX IF NOT EXISTS idx_flexible_layouts_field ON flexible_layouts(field_id);
|
|
603
|
+
|
|
604
|
+
-- ============================================================
|
|
605
|
+
-- Taxonomy Terms & Content Terms (DECI-022 binding por referência)
|
|
606
|
+
-- ============================================================
|
|
607
|
+
CREATE TABLE IF NOT EXISTS taxonomy_terms (
|
|
608
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
609
|
+
taxonomy_id UUID NOT NULL REFERENCES taxonomies(id) ON DELETE CASCADE,
|
|
610
|
+
tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
|
|
611
|
+
name VARCHAR(255) NOT NULL,
|
|
612
|
+
slug VARCHAR(255) NOT NULL,
|
|
613
|
+
description TEXT,
|
|
614
|
+
parent_id UUID REFERENCES taxonomy_terms(id) ON DELETE SET NULL,
|
|
615
|
+
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
616
|
+
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
617
|
+
UNIQUE(taxonomy_id, slug)
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
CREATE INDEX IF NOT EXISTS idx_taxonomy_terms_taxonomy ON taxonomy_terms(taxonomy_id);
|
|
621
|
+
CREATE INDEX IF NOT EXISTS idx_taxonomy_terms_parent ON taxonomy_terms(parent_id) WHERE parent_id IS NOT NULL;
|
|
622
|
+
|
|
623
|
+
CREATE TABLE IF NOT EXISTS content_taxonomy_terms (
|
|
624
|
+
content_id UUID NOT NULL REFERENCES content(id) ON DELETE CASCADE,
|
|
625
|
+
term_id UUID NOT NULL REFERENCES taxonomy_terms(id) ON DELETE CASCADE,
|
|
626
|
+
PRIMARY KEY (content_id, term_id)
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
CREATE INDEX IF NOT EXISTS idx_content_terms_content ON content_taxonomy_terms(content_id);
|
|
630
|
+
CREATE INDEX IF NOT EXISTS idx_content_terms_term ON content_taxonomy_terms(term_id);
|
|
631
|
+
|
|
632
|
+
-- ============================================================
|
|
633
|
+
-- Row Level Security Policies
|
|
634
|
+
-- ============================================================
|
|
635
|
+
|
|
636
|
+
-- Enable RLS on all tenant-scoped tables
|
|
637
|
+
ALTER TABLE content ENABLE ROW LEVEL SECURITY;
|
|
638
|
+
ALTER TABLE media ENABLE ROW LEVEL SECURITY;
|
|
639
|
+
ALTER TABLE categories ENABLE ROW LEVEL SECURITY;
|
|
640
|
+
ALTER TABLE tags ENABLE ROW LEVEL SECURITY;
|
|
641
|
+
ALTER TABLE menus ENABLE ROW LEVEL SECURITY;
|
|
642
|
+
ALTER TABLE settings ENABLE ROW LEVEL SECURITY;
|
|
643
|
+
ALTER TABLE plugins ENABLE ROW LEVEL SECURITY;
|
|
644
|
+
ALTER TABLE themes ENABLE ROW LEVEL SECURITY;
|
|
645
|
+
ALTER TABLE builds ENABLE ROW LEVEL SECURITY;
|
|
646
|
+
ALTER TABLE deployments ENABLE ROW LEVEL SECURITY;
|
|
647
|
+
ALTER TABLE events ENABLE ROW LEVEL SECURITY;
|
|
648
|
+
ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY;
|
|
649
|
+
ALTER TABLE sessions ENABLE ROW LEVEL SECURITY;
|
|
650
|
+
ALTER TABLE webhooks ENABLE ROW LEVEL SECURITY;
|
|
651
|
+
|
|
652
|
+
-- RLS Policies using app.current_tenant_id session variable
|
|
653
|
+
CREATE POLICY tenant_isolation_content ON content
|
|
654
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
655
|
+
|
|
656
|
+
CREATE POLICY tenant_isolation_media ON media
|
|
657
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
658
|
+
|
|
659
|
+
CREATE POLICY tenant_isolation_categories ON categories
|
|
660
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
661
|
+
|
|
662
|
+
CREATE POLICY tenant_isolation_tags ON tags
|
|
663
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
664
|
+
|
|
665
|
+
CREATE POLICY tenant_isolation_menus ON menus
|
|
666
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
667
|
+
|
|
668
|
+
CREATE POLICY tenant_isolation_settings ON settings
|
|
669
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid OR tenant_id IS NULL);
|
|
670
|
+
|
|
671
|
+
CREATE POLICY tenant_isolation_plugins ON plugins
|
|
672
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid OR tenant_id IS NULL);
|
|
673
|
+
|
|
674
|
+
CREATE POLICY tenant_isolation_themes ON themes
|
|
675
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid OR tenant_id IS NULL);
|
|
676
|
+
|
|
677
|
+
CREATE POLICY tenant_isolation_builds ON builds
|
|
678
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
679
|
+
|
|
680
|
+
CREATE POLICY tenant_isolation_deployments ON deployments
|
|
681
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
682
|
+
|
|
683
|
+
CREATE POLICY tenant_isolation_events ON events
|
|
684
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
685
|
+
|
|
686
|
+
CREATE POLICY tenant_isolation_audit_logs ON audit_logs
|
|
687
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
688
|
+
|
|
689
|
+
CREATE POLICY tenant_isolation_sessions ON sessions
|
|
690
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
691
|
+
|
|
692
|
+
CREATE POLICY tenant_isolation_webhooks ON webhooks
|
|
693
|
+
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oktis-works/database",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc && cp src/schema/schema.sql dist/schema/schema.sql",
|
|
16
|
+
"dev": "tsc --watch",
|
|
17
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
18
|
+
"db:migrate": "node --loader ts-node/esm src/migrations/runner.ts",
|
|
19
|
+
"db:rollback": "node --loader ts-node/esm src/migrations/runner.ts rollback"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@oktis-works/types": "0.1.0",
|
|
23
|
+
"@oktis-works/config": "0.1.0",
|
|
24
|
+
"postgres": "3.4.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "5.7.3",
|
|
28
|
+
"ts-node": "10.9.0"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public",
|
|
35
|
+
"registry": "https://registry.npmjs.org/"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/blacklotus/blacklotus-cms",
|
|
41
|
+
"directory": "packages/database"
|
|
42
|
+
}
|
|
43
|
+
}
|