@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.
@@ -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"}
@@ -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,3 @@
1
+ export declare function getSchema(): string;
2
+ export declare const schema: string;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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
+ }