@mybuildex/pkg 0.10.3 → 0.12.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/README.md +34 -0
- package/dist/acl/map.generated.js +30 -0
- package/dist/acl/map.generated.js.map +1 -1
- package/dist/acl/plugin.d.ts +7 -0
- package/dist/acl/plugin.js +15 -0
- package/dist/acl/plugin.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +34 -1
- package/dist/index.js.map +1 -1
- package/dist/network/context.d.ts +70 -0
- package/dist/network/context.js +196 -0
- package/dist/network/context.js.map +1 -0
- package/dist/network/plugin.d.ts +21 -0
- package/dist/network/plugin.js +222 -0
- package/dist/network/plugin.js.map +1 -0
- package/dist/network/replication.d.ts +86 -0
- package/dist/network/replication.js +328 -0
- package/dist/network/replication.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.METHOD_ACTIONS = exports.ROUTE_KEY_ACTIONS = void 0;
|
|
4
|
+
exports.routeTable = routeTable;
|
|
5
|
+
exports.routeAction = routeAction;
|
|
6
|
+
exports.createNetworkPlugin = createNetworkPlugin;
|
|
7
|
+
// Plugin da governança de rede (ver context.ts e replication.ts).
|
|
8
|
+
//
|
|
9
|
+
// Regra (22/10/2026): sem governança, a ACL decide sozinha; com governança, a
|
|
10
|
+
// ACL valida PRIMEIRO e a governança em seguida — se a rede não libera, barra
|
|
11
|
+
// mesmo com a permissão da ACL. A governança nunca barra sozinha.
|
|
12
|
+
//
|
|
13
|
+
// guard da rota instalado no onRoute, DEPOIS do guard da ACL (o
|
|
14
|
+
// useModuleAcl põe o dele na frente da lista). Resolve o
|
|
15
|
+
// contexto da escola (cache curto). Filial: nega com 403 a
|
|
16
|
+
// ação que a rede não libera na tabela da rota — só quando a
|
|
17
|
+
// ACL comanda a rota (enforce). Matriz: troca request.prisma
|
|
18
|
+
// pelo client que anota as escritas.
|
|
19
|
+
// onSend matriz: replica nas filiais o que foi escrito, ANTES da
|
|
20
|
+
// resposta sair (no Cloud Run a CPU é cortada depois dela).
|
|
21
|
+
//
|
|
22
|
+
// O guard vem antes dos preHandlers da própria rota (setRequest dos
|
|
23
|
+
// controllers), que já recebem o client trocado. Registrar DEPOIS do
|
|
24
|
+
// createAclPlugin e ANTES do plugin de compressão e dos módulos: o onSend
|
|
25
|
+
// daqui pode trocar o corpo por um erro, e o onRoute só vale para as rotas
|
|
26
|
+
// registradas depois dele.
|
|
27
|
+
//
|
|
28
|
+
// NETWORK_GOVERNANCE=off desliga tudo (chave de emergência, como ACL_MODE).
|
|
29
|
+
const context_1 = require("./context");
|
|
30
|
+
const replication_1 = require("./replication");
|
|
31
|
+
const plugin_1 = require("../acl/plugin");
|
|
32
|
+
const NET_GUARD = Symbol.for("@mybuildex/pkg/networkGuard");
|
|
33
|
+
/** Ação da rede para cada constante do router (useModuleAcl grava em config.routeAcl.key). */
|
|
34
|
+
exports.ROUTE_KEY_ACTIONS = {
|
|
35
|
+
ALL: "read",
|
|
36
|
+
FIND: "read",
|
|
37
|
+
PAGINATE: "read",
|
|
38
|
+
TRASH: "read",
|
|
39
|
+
CREATE: "add",
|
|
40
|
+
CLONE: "add",
|
|
41
|
+
CREATE_FILE: "add",
|
|
42
|
+
UPDATE: "edit",
|
|
43
|
+
UPDATE_FILE: "edit",
|
|
44
|
+
UPDATE_ITENS: "edit",
|
|
45
|
+
SAVE: "edit",
|
|
46
|
+
SYNC: "edit",
|
|
47
|
+
REMOVE: "remove",
|
|
48
|
+
RESTORE: "restore",
|
|
49
|
+
DELETE: "delete",
|
|
50
|
+
};
|
|
51
|
+
/** Rota sem constante conhecida: decide pelo método. */
|
|
52
|
+
exports.METHOD_ACTIONS = {
|
|
53
|
+
GET: "read",
|
|
54
|
+
HEAD: "read",
|
|
55
|
+
POST: "add",
|
|
56
|
+
PUT: "edit",
|
|
57
|
+
PATCH: "edit",
|
|
58
|
+
DELETE: "delete",
|
|
59
|
+
};
|
|
60
|
+
const ACTION_LABELS = {
|
|
61
|
+
read: "consultar",
|
|
62
|
+
add: "incluir",
|
|
63
|
+
edit: "editar",
|
|
64
|
+
remove: "remover",
|
|
65
|
+
restore: "restaurar",
|
|
66
|
+
delete: "excluir",
|
|
67
|
+
};
|
|
68
|
+
/** Tabela da rota: o segmento depois de /v1/:id_school (o NAME do router). */
|
|
69
|
+
function routeTable(url) {
|
|
70
|
+
const m = /^\/v1\/:id_school\/([A-Za-z0-9_]+)/.exec(String(url ?? ""));
|
|
71
|
+
return m ? m[1] : null;
|
|
72
|
+
}
|
|
73
|
+
function routeAction(method, key) {
|
|
74
|
+
if (key && exports.ROUTE_KEY_ACTIONS[key])
|
|
75
|
+
return exports.ROUTE_KEY_ACTIONS[key];
|
|
76
|
+
return exports.METHOD_ACTIONS[String(method).toUpperCase()] ?? null;
|
|
77
|
+
}
|
|
78
|
+
function defaultLogger(event, data) {
|
|
79
|
+
const severity = event.endsWith("failed") ? "ERROR" : event === "network_replicated" ? "INFO" : "WARNING";
|
|
80
|
+
const line = JSON.stringify({ severity, event, ...data });
|
|
81
|
+
if (severity === "ERROR")
|
|
82
|
+
console.error(line);
|
|
83
|
+
else if (severity === "INFO")
|
|
84
|
+
console.log(line);
|
|
85
|
+
else
|
|
86
|
+
console.warn(line);
|
|
87
|
+
}
|
|
88
|
+
function createNetworkPlugin(options = {}) {
|
|
89
|
+
const enabled = options.enabled ?? process.env.NETWORK_GOVERNANCE !== "off";
|
|
90
|
+
const ttlMs = options.ttlMs ?? Number(process.env.NETWORK_CACHE_TTL_MS ?? 30_000);
|
|
91
|
+
const log = options.logger ?? defaultLogger;
|
|
92
|
+
const contexts = (0, context_1.createNetworkContextCache)(Number.isFinite(ttlMs) && ttlMs >= 0 ? ttlMs : 30_000, options.load);
|
|
93
|
+
const tableMeta = (0, replication_1.createTableMetaCache)();
|
|
94
|
+
const plugin = async function networkPlugin(fastify) {
|
|
95
|
+
if (!enabled)
|
|
96
|
+
return;
|
|
97
|
+
const unavailable = (reply) => reply.code(503).send({ statusCode: 503, error: "Service Unavailable", message: "Não foi possível validar as regras da rede de escolas. Tente novamente." });
|
|
98
|
+
const guard = async function networkGuard(request, reply) {
|
|
99
|
+
const idSchool = request.id_school;
|
|
100
|
+
if (!idSchool || !request.prisma || request.networkPending)
|
|
101
|
+
return;
|
|
102
|
+
const url = request.routeOptions?.url;
|
|
103
|
+
const table = routeTable(url);
|
|
104
|
+
// rota sem escola na URL (documentação, audit/archive, templates)
|
|
105
|
+
if (!table)
|
|
106
|
+
return;
|
|
107
|
+
// a governança só barra junto com a ACL (que já validou: o guard
|
|
108
|
+
// dela roda antes deste)
|
|
109
|
+
const withAcl = (0, plugin_1.aclEnforces)(request);
|
|
110
|
+
let ctx;
|
|
111
|
+
try {
|
|
112
|
+
ctx = await contexts.get(request.prisma, String(idSchool));
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
// a escola É de rede (a flag foi lida) e o core não respondeu
|
|
116
|
+
const stale = contexts.stale(request.prisma, String(idSchool));
|
|
117
|
+
log("network_context_failed", { id_school: idSchool, route: url, table, stale: !!stale, error: e?.message ?? String(e) });
|
|
118
|
+
if (stale && !stale.moduleOf.has(table)) {
|
|
119
|
+
// tabela fora do catálogo (último catálogo conhecido): sem
|
|
120
|
+
// governança, a ACL decide sozinha
|
|
121
|
+
ctx = stale;
|
|
122
|
+
}
|
|
123
|
+
else if (!withAcl) {
|
|
124
|
+
// a governança não barra sozinha
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// tabela governada (ou catálogo nunca carregado): sem as
|
|
129
|
+
// regras não dá para liberar (fail-closed)
|
|
130
|
+
return unavailable(reply);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
request.network = ctx;
|
|
134
|
+
if (ctx.role === "filial") {
|
|
135
|
+
if (!withAcl)
|
|
136
|
+
return;
|
|
137
|
+
const key = request.routeOptions?.config?.routeAcl?.key;
|
|
138
|
+
const action = routeAction(request.method, key);
|
|
139
|
+
if (action && !(0, context_1.networkAllows)(ctx, table, action)) {
|
|
140
|
+
request.networkDenied = true;
|
|
141
|
+
log("network_denied", {
|
|
142
|
+
id_school: idSchool, id_network: ctx.idNetwork, method: request.method, route: url,
|
|
143
|
+
table, action, uid: request.user_id ?? request.user?.uid,
|
|
144
|
+
});
|
|
145
|
+
return reply.code(403).send({
|
|
146
|
+
statusCode: 403,
|
|
147
|
+
error: "Forbidden",
|
|
148
|
+
message: `A governança da rede não permite ${ACTION_LABELS[action]} ${table} nesta escola. Alterações são feitas pela escola matriz.`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// matriz: a réplica não barra ninguém, só mantém as filiais iguais
|
|
154
|
+
// à matriz — vale mesmo com a ACL fora do enforce
|
|
155
|
+
if (ctx.role === "matriz" && ctx.filiais.length > 0 && ctx.moduleOf.size > 0) {
|
|
156
|
+
const base = request.prisma;
|
|
157
|
+
const writes = new replication_1.NetworkWrites();
|
|
158
|
+
const pending = { ctx, base, writes };
|
|
159
|
+
request.networkPending = pending;
|
|
160
|
+
request.prisma = (0, replication_1.trackWrites)(base, {
|
|
161
|
+
tables: new Set(ctx.moduleOf.keys()),
|
|
162
|
+
base,
|
|
163
|
+
writes,
|
|
164
|
+
meta: (t) => tableMeta(base, String(base.$schema), t),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
guard[NET_GUARD] = true;
|
|
169
|
+
// Na frente dos preHandlers da rota. O useModuleAcl (onRoute do
|
|
170
|
+
// controller, roda depois deste) põe o guard da ACL na frente de tudo:
|
|
171
|
+
// a ordem final é ACL -> rede -> preHandlers da rota.
|
|
172
|
+
fastify.addHook("onRoute", (routeOptions) => {
|
|
173
|
+
const existing = routeOptions.preHandler;
|
|
174
|
+
const list = existing == null ? [] : Array.isArray(existing) ? existing : [existing];
|
|
175
|
+
// HEAD automático do GET herda a lista já montada: não duplica
|
|
176
|
+
if (list.some((fn) => fn?.[NET_GUARD]))
|
|
177
|
+
return;
|
|
178
|
+
routeOptions.preHandler = [guard, ...list];
|
|
179
|
+
});
|
|
180
|
+
fastify.addHook("onSend", async (request, reply, payload) => {
|
|
181
|
+
const pending = request.networkPending;
|
|
182
|
+
if (!pending || pending.writes.empty)
|
|
183
|
+
return payload;
|
|
184
|
+
request.networkPending = undefined;
|
|
185
|
+
const { ctx, base, writes } = pending;
|
|
186
|
+
const source = String(base.$schema);
|
|
187
|
+
try {
|
|
188
|
+
const result = await (0, replication_1.syncWrites)({
|
|
189
|
+
db: base,
|
|
190
|
+
base,
|
|
191
|
+
source,
|
|
192
|
+
targets: ctx.filiais.map(id => (0, context_1.schoolDatabaseOf)(source, id)),
|
|
193
|
+
writes,
|
|
194
|
+
meta: (t) => tableMeta(base, source, t),
|
|
195
|
+
});
|
|
196
|
+
log("network_replicated", {
|
|
197
|
+
id_school: ctx.idSchool, id_network: ctx.idNetwork, route: request.routeOptions?.url,
|
|
198
|
+
filiais: ctx.filiais, tables: result.tables, statements: result.statements,
|
|
199
|
+
});
|
|
200
|
+
return payload;
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
log("network_replication_failed", {
|
|
204
|
+
id_school: ctx.idSchool, id_network: ctx.idNetwork, route: request.routeOptions?.url,
|
|
205
|
+
filiais: ctx.filiais, tables: writes.tables, error: e?.message ?? String(e),
|
|
206
|
+
});
|
|
207
|
+
// resposta de erro original fica como está; sucesso vira erro:
|
|
208
|
+
// a matriz gravou, mas as filiais não (a réplica é atômica)
|
|
209
|
+
if (reply.statusCode >= 400)
|
|
210
|
+
return payload;
|
|
211
|
+
reply.code(500);
|
|
212
|
+
reply.header("content-type", "application/json; charset=utf-8");
|
|
213
|
+
return JSON.stringify({
|
|
214
|
+
statusCode: 500,
|
|
215
|
+
message: `Salvo na escola matriz, mas a replicação para as escolas da rede (${ctx.filiais.join(", ")}) falhou e nenhuma delas foi alterada: ${e?.message ?? e}`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
return Object.assign(plugin, { clearCache: () => contexts.clear() });
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/network/plugin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** Escritas do Prisma que a réplica acompanha (as mesmas da auditoria). */
|
|
2
|
+
export declare const NETWORK_WRITE_OPERATIONS: Set<string>;
|
|
3
|
+
/** Tabelas nunca replicadas, mesmo no catálogo. */
|
|
4
|
+
export declare const NEVER_REPLICATED: Set<string>;
|
|
5
|
+
/**
|
|
6
|
+
* Linhas que pertencem a cada escola e nunca são copiadas nem apagadas pela
|
|
7
|
+
* réplica: a flag REDE é gravada pelo core em cada membro.
|
|
8
|
+
*/
|
|
9
|
+
export declare const PROTECTED_ROWS: Readonly<Record<string, {
|
|
10
|
+
column: string;
|
|
11
|
+
value: string;
|
|
12
|
+
}>>;
|
|
13
|
+
/** Teto de linhas por escrita *Many acompanhada. */
|
|
14
|
+
export declare const MAX_TRACKED_ROWS = 5000;
|
|
15
|
+
export interface TableMeta {
|
|
16
|
+
/** Colunas graváveis (sem as GENERATED), na ordem da tabela. */
|
|
17
|
+
columns: string[];
|
|
18
|
+
/** Chave primária. */
|
|
19
|
+
pk: string[];
|
|
20
|
+
}
|
|
21
|
+
type Row = Record<string, any>;
|
|
22
|
+
/**
|
|
23
|
+
* Colunas e chave primária da tabela (information_schema). As tabelas são as
|
|
24
|
+
* mesmas em todas as escolas: cache por tabela, para a vida da instância.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createTableMetaCache(): (db: any, schema: string, table: string) => Promise<TableMeta>;
|
|
27
|
+
/**
|
|
28
|
+
* where do Prisma -> where de findMany: chave única composta
|
|
29
|
+
* (`{a_b: {a, b}}`) vira `{a, b}`; o resto passa igual.
|
|
30
|
+
*/
|
|
31
|
+
export declare function flattenWhere(where: Row | undefined, meta: TableMeta): Row;
|
|
32
|
+
/** Linhas tocadas pela request na matriz, por tabela, na ordem da primeira escrita. */
|
|
33
|
+
export declare class NetworkWrites {
|
|
34
|
+
readonly tables: string[];
|
|
35
|
+
readonly keys: Map<string, Map<string, Row>>;
|
|
36
|
+
readonly lookups: Map<string, Row[]>;
|
|
37
|
+
private touch;
|
|
38
|
+
addKey(table: string, key: Row, pk: string[]): void;
|
|
39
|
+
addLookup(table: string, where: Row): void;
|
|
40
|
+
get empty(): boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface TrackOptions {
|
|
43
|
+
/** Tabelas replicadas (catálogo da rede). */
|
|
44
|
+
tables: ReadonlySet<string>;
|
|
45
|
+
/** Client SEM o proxy, do mesmo schema: lê as chaves antes da escrita. */
|
|
46
|
+
base: any;
|
|
47
|
+
meta: (table: string) => Promise<TableMeta>;
|
|
48
|
+
writes: NetworkWrites;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Client da matriz que anota as escritas nas tabelas replicadas. Leitura, SQL
|
|
52
|
+
* cru e tabelas fora do catálogo passam direto. $transaction interativa
|
|
53
|
+
* recebe um tx também acompanhado.
|
|
54
|
+
*/
|
|
55
|
+
export declare function trackWrites<T extends object>(client: T, opts: TrackOptions): T;
|
|
56
|
+
export interface SyncStatement {
|
|
57
|
+
sql: string;
|
|
58
|
+
params: any[];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* SQL que deixa as linhas `keys` de `table` na filial iguais às da matriz:
|
|
62
|
+
* apaga as que sumiram da matriz e insere/atualiza as demais com o mesmo id.
|
|
63
|
+
*/
|
|
64
|
+
export declare function buildSyncStatements(source: string, target: string, table: string, meta: TableMeta, keys: Row[]): {
|
|
65
|
+
deletes: SyncStatement[];
|
|
66
|
+
upserts: SyncStatement[];
|
|
67
|
+
};
|
|
68
|
+
export interface SyncResult {
|
|
69
|
+
tables: Record<string, number>;
|
|
70
|
+
filiais: string[];
|
|
71
|
+
statements: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Copia para cada filial as linhas anotadas em `writes`, numa transação só.
|
|
75
|
+
* `db` executa o SQL (qualquer client do pool serve: todo nome é qualificado)
|
|
76
|
+
* e `base` reencontra as linhas do createMany na matriz.
|
|
77
|
+
*/
|
|
78
|
+
export declare function syncWrites(input: {
|
|
79
|
+
db: any;
|
|
80
|
+
base: any;
|
|
81
|
+
source: string;
|
|
82
|
+
targets: string[];
|
|
83
|
+
writes: NetworkWrites;
|
|
84
|
+
meta: (table: string) => Promise<TableMeta>;
|
|
85
|
+
}): Promise<SyncResult>;
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Replicação matriz -> filiais (governança de rede; ver context.ts).
|
|
3
|
+
//
|
|
4
|
+
// Tudo o que a MATRIZ grava numa tabela do catálogo de módulos da rede é
|
|
5
|
+
// espelhado nas filiais — os bancos das escolas têm as mesmas tabelas. Em vez
|
|
6
|
+
// de repetir cada operação do Prisma em cada filial (ids divergiriam, e
|
|
7
|
+
// update/delete por id acertariam a linha errada), a réplica copia o ESTADO
|
|
8
|
+
// final das linhas tocadas, pela chave primária:
|
|
9
|
+
//
|
|
10
|
+
// 1. durante a request, o client da matriz (proxy de trackWrites) anota a
|
|
11
|
+
// chave de cada linha escrita: pelo resultado (create/update/upsert/
|
|
12
|
+
// delete), por uma leitura antes da escrita (where de update/delete e
|
|
13
|
+
// das variantes *Many) ou, no createMany (o MySQL não devolve os ids),
|
|
14
|
+
// por uma busca depois com os mesmos valores gravados;
|
|
15
|
+
// 2. no fim da request (onSend, ANTES da resposta sair), syncWrites roda numa
|
|
16
|
+
// ÚNICA transação, para cada filial:
|
|
17
|
+
// DELETE das linhas que não existem mais na matriz
|
|
18
|
+
// INSERT ... SELECT da matriz ... ON DUPLICATE KEY UPDATE
|
|
19
|
+
// com o mesmo id da matriz. Falha em qualquer filial desfaz a réplica em
|
|
20
|
+
// todas (a escrita na matriz já foi gravada).
|
|
21
|
+
//
|
|
22
|
+
// Por copiar o estado (e não a operação), a réplica é idempotente e também
|
|
23
|
+
// cobre escrita dentro de $transaction (o estado lido já é o confirmado).
|
|
24
|
+
//
|
|
25
|
+
// Limitações: SQL cru ($executeRaw) na matriz não é capturado; escrita
|
|
26
|
+
// aninhada em relação (data: {filhos: {create}}) só replica a tabela do model
|
|
27
|
+
// chamado.
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.NetworkWrites = exports.MAX_TRACKED_ROWS = exports.PROTECTED_ROWS = exports.NEVER_REPLICATED = exports.NETWORK_WRITE_OPERATIONS = void 0;
|
|
30
|
+
exports.createTableMetaCache = createTableMetaCache;
|
|
31
|
+
exports.flattenWhere = flattenWhere;
|
|
32
|
+
exports.trackWrites = trackWrites;
|
|
33
|
+
exports.buildSyncStatements = buildSyncStatements;
|
|
34
|
+
exports.syncWrites = syncWrites;
|
|
35
|
+
/** Escritas do Prisma que a réplica acompanha (as mesmas da auditoria). */
|
|
36
|
+
exports.NETWORK_WRITE_OPERATIONS = new Set([
|
|
37
|
+
"create", "createMany", "createManyAndReturn",
|
|
38
|
+
"update", "updateMany", "updateManyAndReturn",
|
|
39
|
+
"upsert", "delete", "deleteMany",
|
|
40
|
+
]);
|
|
41
|
+
/** Leitura da chave antes de escrever (where de update/delete/upsert/*Many). */
|
|
42
|
+
const READ_KEYS_BEFORE = new Set(["update", "updateMany", "updateManyAndReturn", "upsert", "delete", "deleteMany"]);
|
|
43
|
+
/** Tabelas nunca replicadas, mesmo no catálogo. */
|
|
44
|
+
exports.NEVER_REPLICATED = new Set(["audit_logs", "migrations"]);
|
|
45
|
+
/**
|
|
46
|
+
* Linhas que pertencem a cada escola e nunca são copiadas nem apagadas pela
|
|
47
|
+
* réplica: a flag REDE é gravada pelo core em cada membro.
|
|
48
|
+
*/
|
|
49
|
+
exports.PROTECTED_ROWS = {
|
|
50
|
+
configs: { column: "key", value: "REDE" },
|
|
51
|
+
};
|
|
52
|
+
/** Teto de linhas por escrita *Many acompanhada. */
|
|
53
|
+
exports.MAX_TRACKED_ROWS = 5000;
|
|
54
|
+
const CHUNK = 500;
|
|
55
|
+
const IDENT_RE = /^[A-Za-z0-9_]+$/;
|
|
56
|
+
function q(ident) {
|
|
57
|
+
if (!IDENT_RE.test(ident))
|
|
58
|
+
throw new Error(`identificador inválido: ${ident}`);
|
|
59
|
+
return `\`${ident}\``;
|
|
60
|
+
}
|
|
61
|
+
function isPlainObject(v) {
|
|
62
|
+
return !!v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date) && Object.getPrototypeOf(v) === Object.prototype;
|
|
63
|
+
}
|
|
64
|
+
function isPrimitive(v) {
|
|
65
|
+
return v === null || ["string", "number", "bigint", "boolean"].includes(typeof v);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Colunas e chave primária da tabela (information_schema). As tabelas são as
|
|
69
|
+
* mesmas em todas as escolas: cache por tabela, para a vida da instância.
|
|
70
|
+
*/
|
|
71
|
+
function createTableMetaCache() {
|
|
72
|
+
const cache = new Map();
|
|
73
|
+
return function tableMeta(db, schema, table) {
|
|
74
|
+
let hit = cache.get(table);
|
|
75
|
+
if (!hit) {
|
|
76
|
+
hit = (async () => {
|
|
77
|
+
const rows = await db.$queryRawUnsafe("SELECT COLUMN_NAME AS name, COLUMN_KEY AS col_key, EXTRA AS extra FROM information_schema.COLUMNS" +
|
|
78
|
+
" WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION", schema, table);
|
|
79
|
+
const columns = rows.filter(r => !/GENERATED/i.test(String(r.extra ?? ""))).map(r => String(r.name));
|
|
80
|
+
const pk = rows.filter(r => String(r.col_key) === "PRI").map(r => String(r.name));
|
|
81
|
+
if (!columns.length)
|
|
82
|
+
throw new Error(`tabela ${schema}.${table} não encontrada`);
|
|
83
|
+
if (!pk.length)
|
|
84
|
+
throw new Error(`tabela ${table} sem chave primária: não dá para replicar`);
|
|
85
|
+
return { columns, pk };
|
|
86
|
+
})();
|
|
87
|
+
cache.set(table, hit);
|
|
88
|
+
hit.catch(() => cache.delete(table));
|
|
89
|
+
}
|
|
90
|
+
return hit;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function keyOf(row, pk) {
|
|
94
|
+
if (!row || typeof row !== "object")
|
|
95
|
+
return null;
|
|
96
|
+
const key = {};
|
|
97
|
+
for (const c of pk) {
|
|
98
|
+
const v = row[c];
|
|
99
|
+
if (v === undefined || v === null || typeof v === "object")
|
|
100
|
+
return null;
|
|
101
|
+
key[c] = v;
|
|
102
|
+
}
|
|
103
|
+
return key;
|
|
104
|
+
}
|
|
105
|
+
function keyString(key, pk) {
|
|
106
|
+
return pk.map(c => String(key[c])).join("\u0000");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* where do Prisma -> where de findMany: chave única composta
|
|
110
|
+
* (`{a_b: {a, b}}`) vira `{a, b}`; o resto passa igual.
|
|
111
|
+
*/
|
|
112
|
+
function flattenWhere(where, meta) {
|
|
113
|
+
const out = {};
|
|
114
|
+
for (const [k, v] of Object.entries(where ?? {})) {
|
|
115
|
+
if (!meta.columns.includes(k) && isPlainObject(v) && Object.keys(v).length > 0 && Object.keys(v).every(c => meta.columns.includes(c))) {
|
|
116
|
+
Object.assign(out, v);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
out[k] = v;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/** Valores simples gravados numa linha (para reencontrá-la depois do createMany). */
|
|
125
|
+
function lookupWhere(data, meta) {
|
|
126
|
+
const where = {};
|
|
127
|
+
for (const [k, v] of Object.entries(data ?? {})) {
|
|
128
|
+
if (meta.columns.includes(k) && isPrimitive(v))
|
|
129
|
+
where[k] = v;
|
|
130
|
+
}
|
|
131
|
+
return Object.keys(where).length ? where : null;
|
|
132
|
+
}
|
|
133
|
+
/** Linhas tocadas pela request na matriz, por tabela, na ordem da primeira escrita. */
|
|
134
|
+
class NetworkWrites {
|
|
135
|
+
tables = [];
|
|
136
|
+
keys = new Map();
|
|
137
|
+
lookups = new Map();
|
|
138
|
+
touch(table) {
|
|
139
|
+
if (!this.keys.has(table)) {
|
|
140
|
+
this.tables.push(table);
|
|
141
|
+
this.keys.set(table, new Map());
|
|
142
|
+
this.lookups.set(table, []);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
addKey(table, key, pk) {
|
|
146
|
+
this.touch(table);
|
|
147
|
+
this.keys.get(table).set(keyString(key, pk), key);
|
|
148
|
+
}
|
|
149
|
+
addLookup(table, where) {
|
|
150
|
+
this.touch(table);
|
|
151
|
+
this.lookups.get(table).push(where);
|
|
152
|
+
}
|
|
153
|
+
get empty() {
|
|
154
|
+
return this.tables.length === 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
exports.NetworkWrites = NetworkWrites;
|
|
158
|
+
async function trackOperation(table, op, args, run, opts) {
|
|
159
|
+
const meta = await opts.meta(table);
|
|
160
|
+
const { writes, base } = opts;
|
|
161
|
+
if (READ_KEYS_BEFORE.has(op)) {
|
|
162
|
+
const where = flattenWhere(args?.where, meta);
|
|
163
|
+
const select = Object.fromEntries(meta.pk.map(c => [c, true]));
|
|
164
|
+
const before = await base[table].findMany({ where, select, take: exports.MAX_TRACKED_ROWS });
|
|
165
|
+
for (const row of before) {
|
|
166
|
+
const key = keyOf(row, meta.pk);
|
|
167
|
+
if (key)
|
|
168
|
+
writes.addKey(table, key, meta.pk);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const result = await run(args);
|
|
172
|
+
// *Many devolve {count}: sem a chave, keyOf descarta
|
|
173
|
+
const rows = Array.isArray(result) ? result : result && typeof result === "object" ? [result] : [];
|
|
174
|
+
let found = false;
|
|
175
|
+
for (const row of rows) {
|
|
176
|
+
const key = keyOf(row, meta.pk);
|
|
177
|
+
if (key) {
|
|
178
|
+
writes.addKey(table, key, meta.pk);
|
|
179
|
+
found = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!found && (op === "create" || op === "createMany")) {
|
|
183
|
+
// createMany não devolve linhas; create com select sem a chave também não
|
|
184
|
+
const list = op === "create" ? [args?.data] : Array.isArray(args?.data) ? args.data : [args?.data];
|
|
185
|
+
for (const data of list) {
|
|
186
|
+
const key = keyOf(data, meta.pk);
|
|
187
|
+
if (key) {
|
|
188
|
+
writes.addKey(table, key, meta.pk);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const where = lookupWhere(data, meta);
|
|
192
|
+
if (where)
|
|
193
|
+
writes.addLookup(table, where);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
function bindValue(target, prop) {
|
|
199
|
+
const v = Reflect.get(target, prop);
|
|
200
|
+
return typeof v === "function" ? v.bind(target) : v;
|
|
201
|
+
}
|
|
202
|
+
function trackDelegate(delegate, table, opts) {
|
|
203
|
+
return new Proxy(delegate, {
|
|
204
|
+
get(target, prop) {
|
|
205
|
+
const fn = bindValue(target, prop);
|
|
206
|
+
if (typeof prop !== "string" || typeof fn !== "function" || !exports.NETWORK_WRITE_OPERATIONS.has(prop))
|
|
207
|
+
return fn;
|
|
208
|
+
return (args) => trackOperation(table, prop, args, fn, opts);
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Client da matriz que anota as escritas nas tabelas replicadas. Leitura, SQL
|
|
214
|
+
* cru e tabelas fora do catálogo passam direto. $transaction interativa
|
|
215
|
+
* recebe um tx também acompanhado.
|
|
216
|
+
*/
|
|
217
|
+
function trackWrites(client, opts) {
|
|
218
|
+
return new Proxy(client, {
|
|
219
|
+
get(target, prop) {
|
|
220
|
+
if (prop === "$transaction") {
|
|
221
|
+
const tx = bindValue(target, prop);
|
|
222
|
+
return (arg, txOptions) => typeof arg === "function"
|
|
223
|
+
? tx((inner) => arg(trackWrites(inner, opts)), txOptions)
|
|
224
|
+
: tx(arg, txOptions);
|
|
225
|
+
}
|
|
226
|
+
if (typeof prop === "string" && opts.tables.has(prop) && !exports.NEVER_REPLICATED.has(prop)) {
|
|
227
|
+
return trackDelegate(Reflect.get(target, prop), prop, opts);
|
|
228
|
+
}
|
|
229
|
+
return bindValue(target, prop);
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
function keyCondition(alias, pk, keys) {
|
|
234
|
+
if (pk.length === 1) {
|
|
235
|
+
return { sql: `${alias}.${q(pk[0])} IN (${keys.map(() => "?").join(", ")})`, params: keys.map(k => k[pk[0]]) };
|
|
236
|
+
}
|
|
237
|
+
const tuple = `(${pk.map(() => "?").join(", ")})`;
|
|
238
|
+
return {
|
|
239
|
+
sql: `(${pk.map(c => `${alias}.${q(c)}`).join(", ")}) IN (${keys.map(() => tuple).join(", ")})`,
|
|
240
|
+
params: keys.flatMap(k => pk.map(c => k[c])),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function samePk(a, b, pk) {
|
|
244
|
+
return pk.map(c => `${a}.${q(c)} = ${b}.${q(c)}`).join(" AND ");
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* SQL que deixa as linhas `keys` de `table` na filial iguais às da matriz:
|
|
248
|
+
* apaga as que sumiram da matriz e insere/atualiza as demais com o mesmo id.
|
|
249
|
+
*/
|
|
250
|
+
function buildSyncStatements(source, target, table, meta, keys) {
|
|
251
|
+
const src = `${q(source)}.${q(table)}`;
|
|
252
|
+
const dst = `${q(target)}.${q(table)}`;
|
|
253
|
+
const cols = meta.columns.map(q);
|
|
254
|
+
const update = meta.columns.filter(c => !meta.pk.includes(c)).map(c => `${q(c)} = VALUES(${q(c)})`);
|
|
255
|
+
const guard = exports.PROTECTED_ROWS[table];
|
|
256
|
+
const deletes = [];
|
|
257
|
+
const upserts = [];
|
|
258
|
+
for (let i = 0; i < keys.length; i += CHUNK) {
|
|
259
|
+
const chunk = keys.slice(i, i + CHUNK);
|
|
260
|
+
const dk = keyCondition("dst", meta.pk, chunk);
|
|
261
|
+
// DELETE de tabela única com alias (MySQL 8.0.16+): o multi-tabela
|
|
262
|
+
// (`DELETE dst FROM ... AS dst`) exige database default, e o pool não tem
|
|
263
|
+
const del = {
|
|
264
|
+
sql: `DELETE FROM ${dst} AS dst WHERE ${dk.sql}` +
|
|
265
|
+
(guard ? ` AND NOT (dst.${q(guard.column)} <=> ?)` : "") +
|
|
266
|
+
` AND NOT EXISTS (SELECT 1 FROM ${src} AS src WHERE ${samePk("src", "dst", meta.pk)})`,
|
|
267
|
+
params: [...dk.params, ...(guard ? [guard.value] : [])],
|
|
268
|
+
};
|
|
269
|
+
deletes.push(del);
|
|
270
|
+
const sk = keyCondition("src", meta.pk, chunk);
|
|
271
|
+
const head = update.length ? `INSERT INTO ${dst}` : `INSERT IGNORE INTO ${dst}`;
|
|
272
|
+
upserts.push({
|
|
273
|
+
sql: `${head} (${cols.join(", ")}) SELECT ${cols.map(c => `src.${c}`).join(", ")} FROM ${src} AS src WHERE ${sk.sql}` +
|
|
274
|
+
(guard
|
|
275
|
+
? ` AND NOT (src.${q(guard.column)} <=> ?)` +
|
|
276
|
+
` AND NOT EXISTS (SELECT 1 FROM ${dst} AS cur WHERE ${samePk("cur", "src", meta.pk)} AND cur.${q(guard.column)} <=> ?)`
|
|
277
|
+
: "") +
|
|
278
|
+
(update.length ? ` ON DUPLICATE KEY UPDATE ${update.join(", ")}` : ""),
|
|
279
|
+
params: [...sk.params, ...(guard ? [guard.value, guard.value] : [])],
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return { deletes, upserts };
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Copia para cada filial as linhas anotadas em `writes`, numa transação só.
|
|
286
|
+
* `db` executa o SQL (qualquer client do pool serve: todo nome é qualificado)
|
|
287
|
+
* e `base` reencontra as linhas do createMany na matriz.
|
|
288
|
+
*/
|
|
289
|
+
async function syncWrites(input) {
|
|
290
|
+
const { db, base, source, targets, writes, meta } = input;
|
|
291
|
+
const result = { tables: {}, filiais: targets, statements: 0 };
|
|
292
|
+
const perTable = [];
|
|
293
|
+
for (const table of writes.tables) {
|
|
294
|
+
const m = await meta(table);
|
|
295
|
+
const lookups = writes.lookups.get(table) ?? [];
|
|
296
|
+
for (let i = 0; i < lookups.length; i += CHUNK) {
|
|
297
|
+
const select = Object.fromEntries(m.pk.map(c => [c, true]));
|
|
298
|
+
const found = await base[table].findMany({ where: { OR: lookups.slice(i, i + CHUNK) }, select, take: exports.MAX_TRACKED_ROWS });
|
|
299
|
+
for (const row of found) {
|
|
300
|
+
const key = keyOf(row, m.pk);
|
|
301
|
+
if (key)
|
|
302
|
+
writes.addKey(table, key, m.pk);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const keys = [...(writes.keys.get(table)?.values() ?? [])];
|
|
306
|
+
if (keys.length)
|
|
307
|
+
perTable.push({ table, meta: m, keys });
|
|
308
|
+
result.tables[table] = keys.length;
|
|
309
|
+
}
|
|
310
|
+
if (!perTable.length || !targets.length)
|
|
311
|
+
return result;
|
|
312
|
+
const statements = [];
|
|
313
|
+
for (const target of targets) {
|
|
314
|
+
const built = perTable.map(t => buildSyncStatements(source, target, t.table, t.meta, t.keys));
|
|
315
|
+
// filhos antes dos pais ao apagar; pais antes dos filhos ao inserir (FKs)
|
|
316
|
+
for (const b of [...built].reverse())
|
|
317
|
+
statements.push(...b.deletes);
|
|
318
|
+
for (const b of built)
|
|
319
|
+
statements.push(...b.upserts);
|
|
320
|
+
}
|
|
321
|
+
await db.$transaction(async (tx) => {
|
|
322
|
+
for (const s of statements)
|
|
323
|
+
await tx.$executeRawUnsafe(s.sql, ...s.params);
|
|
324
|
+
});
|
|
325
|
+
result.statements = statements.length;
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
//# sourceMappingURL=replication.js.map
|