@jantstack/adonis-authz 1.1.0 → 2.4.0-alpha.1
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/LICENSE +1 -1
- package/README.md +837 -51
- package/build/commands/authz_catalog_diff.d.ts +28 -0
- package/build/commands/authz_catalog_diff.d.ts.map +1 -0
- package/build/commands/authz_catalog_diff.js +67 -0
- package/build/commands/authz_catalog_diff.js.map +1 -0
- package/build/commands/authz_catalog_prune_orphans.d.ts +78 -0
- package/build/commands/authz_catalog_prune_orphans.d.ts.map +1 -0
- package/build/commands/authz_catalog_prune_orphans.js +136 -0
- package/build/commands/authz_catalog_prune_orphans.js.map +1 -0
- package/build/commands/authz_catalog_sync.d.ts +37 -0
- package/build/commands/authz_catalog_sync.d.ts.map +1 -0
- package/build/commands/authz_catalog_sync.js +81 -0
- package/build/commands/authz_catalog_sync.js.map +1 -0
- package/build/commands/authz_freeze.d.ts +44 -0
- package/build/commands/authz_freeze.d.ts.map +1 -0
- package/build/commands/authz_freeze.js +95 -0
- package/build/commands/authz_freeze.js.map +1 -0
- package/build/commands/authz_reconcile.d.ts +102 -0
- package/build/commands/authz_reconcile.d.ts.map +1 -0
- package/build/commands/authz_reconcile.js +294 -0
- package/build/commands/authz_reconcile.js.map +1 -0
- package/build/commands/authz_relations_reconcile.d.ts +73 -0
- package/build/commands/authz_relations_reconcile.d.ts.map +1 -0
- package/build/commands/authz_relations_reconcile.js +225 -0
- package/build/commands/authz_relations_reconcile.js.map +1 -0
- package/build/commands/authz_scopes_relay.d.ts +47 -0
- package/build/commands/authz_scopes_relay.d.ts.map +1 -0
- package/build/commands/authz_scopes_relay.js +141 -0
- package/build/commands/authz_scopes_relay.js.map +1 -0
- package/build/commands/authz_unfreeze.d.ts +37 -0
- package/build/commands/authz_unfreeze.d.ts.map +1 -0
- package/build/commands/authz_unfreeze.js +92 -0
- package/build/commands/authz_unfreeze.js.map +1 -0
- package/build/commands/main.d.ts +8 -1
- package/build/commands/main.d.ts.map +1 -1
- package/build/commands/main.js +8 -1
- package/build/commands/main.js.map +1 -1
- package/build/commands/openfga_provision.d.ts +46 -4
- package/build/commands/openfga_provision.d.ts.map +1 -1
- package/build/commands/openfga_provision.js +91 -7
- package/build/commands/openfga_provision.js.map +1 -1
- package/build/configure.d.ts +11 -0
- package/build/configure.d.ts.map +1 -1
- package/build/configure.js +37 -1
- package/build/configure.js.map +1 -1
- package/build/index.d.ts +75 -7
- package/build/index.d.ts.map +1 -1
- package/build/index.js +67 -5
- package/build/index.js.map +1 -1
- package/build/providers/authz_provider.d.ts +26 -2
- package/build/providers/authz_provider.d.ts.map +1 -1
- package/build/providers/authz_provider.js +48 -2
- package/build/providers/authz_provider.js.map +1 -1
- package/build/services/relations.d.ts +14 -0
- package/build/services/relations.d.ts.map +1 -0
- package/build/services/relations.js +17 -0
- package/build/services/relations.js.map +1 -0
- package/build/src/catalog/catalog.d.ts +289 -0
- package/build/src/catalog/catalog.d.ts.map +1 -0
- package/build/src/catalog/catalog.js +859 -0
- package/build/src/catalog/catalog.js.map +1 -0
- package/build/src/catalog/catalog_cache.d.ts +324 -0
- package/build/src/catalog/catalog_cache.d.ts.map +1 -0
- package/build/src/catalog/catalog_cache.js +666 -0
- package/build/src/catalog/catalog_cache.js.map +1 -0
- package/build/src/clock.d.ts +24 -0
- package/build/src/clock.d.ts.map +1 -0
- package/build/src/clock.js +7 -0
- package/build/src/clock.js.map +1 -0
- package/build/src/define_config.d.ts +201 -7
- package/build/src/define_config.d.ts.map +1 -1
- package/build/src/define_config.js.map +1 -1
- package/build/src/drivers/database_driver.d.ts +324 -15
- package/build/src/drivers/database_driver.d.ts.map +1 -1
- package/build/src/drivers/database_driver.js +1107 -128
- package/build/src/drivers/database_driver.js.map +1 -1
- package/build/src/drivers/database_relations_driver.d.ts +75 -0
- package/build/src/drivers/database_relations_driver.d.ts.map +1 -0
- package/build/src/drivers/database_relations_driver.js +450 -0
- package/build/src/drivers/database_relations_driver.js.map +1 -0
- package/build/src/drivers/openfga_driver.d.ts +963 -55
- package/build/src/drivers/openfga_driver.d.ts.map +1 -1
- package/build/src/drivers/openfga_driver.js +2693 -372
- package/build/src/drivers/openfga_driver.js.map +1 -1
- package/build/src/drivers/openfga_facts.d.ts +369 -0
- package/build/src/drivers/openfga_facts.d.ts.map +1 -0
- package/build/src/drivers/openfga_facts.js +813 -0
- package/build/src/drivers/openfga_facts.js.map +1 -0
- package/build/src/drivers/openfga_relations_driver.d.ts +120 -0
- package/build/src/drivers/openfga_relations_driver.d.ts.map +1 -0
- package/build/src/drivers/openfga_relations_driver.js +466 -0
- package/build/src/drivers/openfga_relations_driver.js.map +1 -0
- package/build/src/errors.d.ts +586 -0
- package/build/src/errors.d.ts.map +1 -1
- package/build/src/errors.js +583 -0
- package/build/src/errors.js.map +1 -1
- package/build/src/expiry.d.ts +27 -0
- package/build/src/expiry.d.ts.map +1 -0
- package/build/src/expiry.js +50 -0
- package/build/src/expiry.js.map +1 -0
- package/build/src/freeze.d.ts +120 -0
- package/build/src/freeze.d.ts.map +1 -0
- package/build/src/freeze.js +172 -0
- package/build/src/freeze.js.map +1 -0
- package/build/src/hierarchical_resolver.d.ts +56 -0
- package/build/src/hierarchical_resolver.d.ts.map +1 -0
- package/build/src/hierarchical_resolver.js +87 -0
- package/build/src/hierarchical_resolver.js.map +1 -0
- package/build/src/{middleware → http}/app_access_middleware.d.ts +8 -6
- package/build/src/http/app_access_middleware.d.ts.map +1 -0
- package/build/src/{middleware → http}/app_access_middleware.js +10 -26
- package/build/src/http/app_access_middleware.js.map +1 -0
- package/build/src/http/resource_access_middleware.d.ts +105 -0
- package/build/src/http/resource_access_middleware.d.ts.map +1 -0
- package/build/src/http/resource_access_middleware.js +81 -0
- package/build/src/http/resource_access_middleware.js.map +1 -0
- package/build/src/identity.d.ts +228 -0
- package/build/src/identity.d.ts.map +1 -0
- package/build/src/identity.js +457 -0
- package/build/src/identity.js.map +1 -0
- package/build/src/manager.d.ts +536 -7
- package/build/src/manager.d.ts.map +1 -1
- package/build/src/manager.js +2676 -23
- package/build/src/manager.js.map +1 -1
- package/build/src/memoize_ancestors.d.ts +23 -0
- package/build/src/memoize_ancestors.d.ts.map +1 -0
- package/build/src/memoize_ancestors.js +42 -0
- package/build/src/memoize_ancestors.js.map +1 -0
- package/build/src/models/authz_assignment.d.ts +11 -11
- package/build/src/models/authz_assignment.d.ts.map +1 -1
- package/build/src/models/authz_deny.d.ts +11 -11
- package/build/src/models/authz_deny.d.ts.map +1 -1
- package/build/src/models/authz_permission.d.ts +17 -11
- package/build/src/models/authz_permission.d.ts.map +1 -1
- package/build/src/models/authz_permission.js +4 -0
- package/build/src/models/authz_permission.js.map +1 -1
- package/build/src/models/authz_role.d.ts +19 -12
- package/build/src/models/authz_role.d.ts.map +1 -1
- package/build/src/models/authz_role.js +6 -1
- package/build/src/models/authz_role.js.map +1 -1
- package/build/src/models/authz_role_permission.d.ts +11 -11
- package/build/src/models/authz_role_permission.d.ts.map +1 -1
- package/build/src/openfga.d.ts +29 -0
- package/build/src/openfga.d.ts.map +1 -0
- package/build/src/openfga.js +26 -0
- package/build/src/openfga.js.map +1 -0
- package/build/src/reconcile.d.ts +37 -0
- package/build/src/reconcile.d.ts.map +1 -0
- package/build/src/reconcile.js +69 -0
- package/build/src/reconcile.js.map +1 -0
- package/build/src/relation_partition_trigger.d.ts +8 -0
- package/build/src/relation_partition_trigger.d.ts.map +1 -0
- package/build/src/relation_partition_trigger.js +85 -0
- package/build/src/relation_partition_trigger.js.map +1 -0
- package/build/src/relations/define_relations_config.d.ts +58 -0
- package/build/src/relations/define_relations_config.d.ts.map +1 -0
- package/build/src/relations/define_relations_config.js +144 -0
- package/build/src/relations/define_relations_config.js.map +1 -0
- package/build/src/relations/manager.d.ts +38 -0
- package/build/src/relations/manager.d.ts.map +1 -0
- package/build/src/relations/manager.js +156 -0
- package/build/src/relations/manager.js.map +1 -0
- package/build/src/relations/reconcile.d.ts +62 -0
- package/build/src/relations/reconcile.d.ts.map +1 -0
- package/build/src/relations/reconcile.js +138 -0
- package/build/src/relations/reconcile.js.map +1 -0
- package/build/src/relations_config_store.d.ts +22 -0
- package/build/src/relations_config_store.d.ts.map +1 -0
- package/build/src/relations_config_store.js +74 -0
- package/build/src/relations_config_store.js.map +1 -0
- package/build/src/scope_outbox.d.ts +69 -0
- package/build/src/scope_outbox.d.ts.map +1 -0
- package/build/src/scope_outbox.js +291 -0
- package/build/src/scope_outbox.js.map +1 -0
- package/build/src/shared/backend_guard.d.ts +106 -0
- package/build/src/shared/backend_guard.d.ts.map +1 -0
- package/build/src/shared/backend_guard.js +246 -0
- package/build/src/shared/backend_guard.js.map +1 -0
- package/build/src/shared/sql_expiry.d.ts +53 -0
- package/build/src/shared/sql_expiry.d.ts.map +1 -0
- package/build/src/shared/sql_expiry.js +66 -0
- package/build/src/shared/sql_expiry.js.map +1 -0
- package/build/src/sql_descendants.d.ts +97 -0
- package/build/src/sql_descendants.d.ts.map +1 -0
- package/build/src/sql_descendants.js +203 -0
- package/build/src/sql_descendants.js.map +1 -0
- package/build/src/testing/contract.d.ts +212 -7
- package/build/src/testing/contract.d.ts.map +1 -1
- package/build/src/testing/contract.js +3449 -24
- package/build/src/testing/contract.js.map +1 -1
- package/build/src/testing/main.d.ts +10 -2
- package/build/src/testing/main.d.ts.map +1 -1
- package/build/src/testing/main.js +5 -1
- package/build/src/testing/main.js.map +1 -1
- package/build/src/testing/migration_contract.d.ts +284 -0
- package/build/src/testing/migration_contract.d.ts.map +1 -0
- package/build/src/testing/migration_contract.js +586 -0
- package/build/src/testing/migration_contract.js.map +1 -0
- package/build/src/testing/relations_contract.d.ts +51 -0
- package/build/src/testing/relations_contract.d.ts.map +1 -0
- package/build/src/testing/relations_contract.js +654 -0
- package/build/src/testing/relations_contract.js.map +1 -0
- package/build/src/testing/relations_reconcile_contract.d.ts +24 -0
- package/build/src/testing/relations_reconcile_contract.d.ts.map +1 -0
- package/build/src/testing/relations_reconcile_contract.js +172 -0
- package/build/src/testing/relations_reconcile_contract.js.map +1 -0
- package/build/src/testing/scope_tree.d.ts +65 -0
- package/build/src/testing/scope_tree.d.ts.map +1 -0
- package/build/src/testing/scope_tree.js +145 -0
- package/build/src/testing/scope_tree.js.map +1 -0
- package/build/src/traits/authz_scopes.d.ts +30 -6
- package/build/src/traits/authz_scopes.d.ts.map +1 -1
- package/build/src/traits/authz_scopes.js +30 -18
- package/build/src/traits/authz_scopes.js.map +1 -1
- package/build/src/traits/has_uuid.d.ts +12 -12
- package/build/src/traits/has_uuid.d.ts.map +1 -1
- package/build/src/types.d.ts +1313 -28
- package/build/src/types.d.ts.map +1 -1
- package/build/src/types.js +20 -4
- package/build/src/types.js.map +1 -1
- package/build/stubs/config/app_acl.stub +4 -2
- package/build/stubs/config/authorization.stub +168 -14
- package/build/stubs/migration.stub +183 -13
- package/build/stubs/scopes_outbox_migration.stub +57 -0
- package/package.json +14 -7
- package/build/commands/openfga_import.d.ts +0 -28
- package/build/commands/openfga_import.d.ts.map +0 -1
- package/build/commands/openfga_import.js +0 -74
- package/build/commands/openfga_import.js.map +0 -1
- package/build/src/catalog.d.ts +0 -3
- package/build/src/catalog.d.ts.map +0 -1
- package/build/src/catalog.js +0 -103
- package/build/src/catalog.js.map +0 -1
- package/build/src/middleware/app_access_middleware.d.ts.map +0 -1
- package/build/src/middleware/app_access_middleware.js.map +0 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { AuthorizationBackendError, AuthorizationBackendTimeoutError, NoScopeResolverError, ScopeResolverError, UnknownScopeError, } from '../errors.js';
|
|
2
|
+
import { APP_SCOPE, APP_SCOPE_TYPE } from '../types.js';
|
|
3
|
+
import { assertScope, scopeSpellings } from '../identity.js';
|
|
4
|
+
/**
|
|
5
|
+
* Clasificación de fallos de dependencias, compartida por ambos drivers.
|
|
6
|
+
*
|
|
7
|
+
* Tres dependencias participan en una pregunta: el catálogo (SQL, en ambos
|
|
8
|
+
* drivers), el árbol del consumidor (`resolveChain`) y el backend de
|
|
9
|
+
* hechos (SQL o FGA). Las tres se clasifican igual: 503 con código propio,
|
|
10
|
+
* la causa original conservada, y NUNCA un `false` ni un error crudo — el
|
|
11
|
+
* invariante 5 tal como está publicado (L0.11, N3).
|
|
12
|
+
*
|
|
13
|
+
* Lo que ya es un error del paquete (422 de catálogo, 500 de programación)
|
|
14
|
+
* pasa intacto: un rol inexistente no es una caída.
|
|
15
|
+
*/
|
|
16
|
+
/** ¿Es un error que este paquete ya clasificó (tiene `code` E_AUTHZ_*)? */
|
|
17
|
+
export function isAuthzError(error) {
|
|
18
|
+
return typeof error?.code === 'string' && error.code.startsWith('E_AUTHZ_');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* ¿Es un error del CLIENTE SQL (pg, mysql2, better-sqlite3, knex) y no del
|
|
22
|
+
* código del consumidor? Por la forma que cada cliente da a sus errores:
|
|
23
|
+
* `pg` un `DatabaseError` con SQLSTATE de 5 caracteres (`25P02`…) y
|
|
24
|
+
* `severity`; `mysql2` un `errno` numérico con `sqlState`; SQLite un `code`
|
|
25
|
+
* `SQLITE_*`; knex un `KnexTimeoutError`. Sirve para clasificar (503) lo que
|
|
26
|
+
* un `fn` del consumidor deja escapar desde dentro de una transacción
|
|
27
|
+
* (2.5-B · K12): un error crudo del cliente nunca cruza la frontera.
|
|
28
|
+
*/
|
|
29
|
+
export function isSqlDriverError(error) {
|
|
30
|
+
const e = error;
|
|
31
|
+
if (!e || typeof e !== 'object')
|
|
32
|
+
return false;
|
|
33
|
+
if (e.name === 'KnexTimeoutError')
|
|
34
|
+
return true;
|
|
35
|
+
if (typeof e.code === 'string' && /^SQLITE_/.test(e.code))
|
|
36
|
+
return true;
|
|
37
|
+
if (typeof e.errno === 'number' && typeof e.sqlState === 'string')
|
|
38
|
+
return true;
|
|
39
|
+
if (typeof e.code === 'string' && /^[0-9A-Z]{5}$/.test(e.code) && ('severity' in e || 'routine' in e))
|
|
40
|
+
return true;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* ¿El error viene de un deadline vencido? knex lanza `KnexTimeoutError` (o, si
|
|
45
|
+
* además falló la cancelación, el error de cancelación con `timeout` puesto);
|
|
46
|
+
* axios usa `ECONNABORTED`/`ETIMEDOUT`, a veces envuelto por el SDK como
|
|
47
|
+
* `cause`. Se mira la cadena entera de causas.
|
|
48
|
+
*/
|
|
49
|
+
export function isTimeoutLike(error) {
|
|
50
|
+
let current = error;
|
|
51
|
+
for (let depth = 0; current && depth < 6; depth++) {
|
|
52
|
+
if (current.name === 'KnexTimeoutError' || typeof current.timeout === 'number')
|
|
53
|
+
return true;
|
|
54
|
+
if (current.code === 'ECONNABORTED' || current.code === 'ETIMEDOUT')
|
|
55
|
+
return true;
|
|
56
|
+
current = current.cause;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Ejecuta una consulta SQL con deadline y traduce su fallo. El builder de
|
|
62
|
+
* Lucid es perezoso (se ejecuta al hacer `await`), así que el `timeout` se
|
|
63
|
+
* fija aquí, en el ÚNICO punto por el que pasan todas las consultas: un
|
|
64
|
+
* call-site nuevo lo hereda sin saberlo. `cancel: true` pide a knex que
|
|
65
|
+
* cancele la consulta en el servidor cuando el dialecto lo permite.
|
|
66
|
+
*/
|
|
67
|
+
export async function guardSql(driver, operation, timeoutMs, fn) {
|
|
68
|
+
try {
|
|
69
|
+
const query = fn();
|
|
70
|
+
if (typeof query?.timeout === 'function') {
|
|
71
|
+
try {
|
|
72
|
+
query.timeout(timeoutMs, { cancel: true });
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// knex rechaza `cancel` al CONSTRUIR la consulta en los dialectos que
|
|
76
|
+
// no saben cancelar (SQLite). El deadline sigue valiendo: vencido, la
|
|
77
|
+
// promesa rechaza y la conexión se descarta; solo no se manda un
|
|
78
|
+
// "cancela" al servidor.
|
|
79
|
+
query.timeout(timeoutMs);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return await query;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (isAuthzError(error))
|
|
86
|
+
throw error;
|
|
87
|
+
if (isTimeoutLike(error)) {
|
|
88
|
+
throw new AuthorizationBackendTimeoutError(driver, operation, timeoutMs, error);
|
|
89
|
+
}
|
|
90
|
+
throw new AuthorizationBackendError(driver, operation, error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Deadline TOTAL de una llamada al backend: vencido ⇒ rechaza con el error
|
|
95
|
+
* que fabrique `onTimeout`. El timer no retiene el event loop y se limpia al
|
|
96
|
+
* resolver. La promesa original sigue su curso —la petición en vuelo puede
|
|
97
|
+
* aterrizar después— pero el llamante ya fue liberado: por eso el manager
|
|
98
|
+
* notifica `indeterminate: true` en las escrituras que vencen (D2) y el
|
|
99
|
+
* driver openfga no deja al SDK reintentar por su cuenta (`maxRetry: 0`).
|
|
100
|
+
*/
|
|
101
|
+
export function withDeadline(promise, ms, onTimeout) {
|
|
102
|
+
let timer;
|
|
103
|
+
const deadline = new Promise((_, reject) => {
|
|
104
|
+
timer = setTimeout(() => reject(onTimeout()), ms);
|
|
105
|
+
timer.unref?.();
|
|
106
|
+
});
|
|
107
|
+
return Promise.race([promise, deadline]).finally(() => clearTimeout(timer));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* El resolutor que usa un driver construido SIN `resolveChain`: solo
|
|
111
|
+
* conoce la raíz. Sustituye al default plano de 1.x (`resto → [APP_SCOPE]`),
|
|
112
|
+
* que hacía descender de `app` cualquier scope inventado o borrado (L0.3).
|
|
113
|
+
* No se lanza al construir el driver, sino en la primera pregunta por un
|
|
114
|
+
* scope intermedio: un despliegue que solo usa `app` es legítimo sin árbol.
|
|
115
|
+
*/
|
|
116
|
+
export const rootOnlyResolver = async (scope) => {
|
|
117
|
+
if (scope.type === APP_SCOPE_TYPE)
|
|
118
|
+
return [APP_SCOPE];
|
|
119
|
+
throw new NoScopeResolverError(`El driver no tiene 'resolveChain' y se preguntó por un scope de tipo '${scope.type}'. ` +
|
|
120
|
+
`Sin resolutor solo existe la raíz 'app': pasa 'scopes.resolveChain' en el config ` +
|
|
121
|
+
`(y 'resolveChain' al driver) para declarar tu jerarquía.`);
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* ¿Dos uuids nombran, con formas distintas, el mismo id? Un motor que
|
|
125
|
+
* canoniza (el tipo `uuid` de PostgreSQL acepta mayúsculas y guiones
|
|
126
|
+
* quitados; una collation `*_ci` funde mayúsculas) puede devolver la fila real
|
|
127
|
+
* para un alias: aquí se acepta ESA relación y nada más. Cualquier otra
|
|
128
|
+
* diferencia es otro scope, y un resolutor que lo devuelva miente.
|
|
129
|
+
*/
|
|
130
|
+
function sameUuidLoosely(canonical, asked) {
|
|
131
|
+
if (canonical === null || asked === null)
|
|
132
|
+
return canonical === asked;
|
|
133
|
+
const fold = (value) => value.toLowerCase().replaceAll('-', '');
|
|
134
|
+
return fold(canonical) === fold(asked);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Cadena CANÓNICA `[scope canónico, ...ancestros]` con el resolutor del
|
|
138
|
+
* consumidor envuelto (2.5-B · K1): el elemento 0 es el scope tal como está
|
|
139
|
+
* en la tabla del consumidor —no tal como lo escribió el llamante—, y es la
|
|
140
|
+
* identidad con la que el paquete lee y escribe TODOS los hechos. Antes se
|
|
141
|
+
* devolvía `[scope del llamante, ...ancestros]`: un alias del uuid (que el
|
|
142
|
+
* árbol fundía con la fila real) resolvía la cadena entera y el grant del
|
|
143
|
+
* ancestro aplicaba, pero el deny —escrito con la forma canónica— no casaba.
|
|
144
|
+
*
|
|
145
|
+
* Si el resolutor lanza, sale como `ScopeResolverError` (503): el árbol es
|
|
146
|
+
* una dependencia más y su caída no es un bug del paquete ni un "sin
|
|
147
|
+
* permiso". `null` = el scope no existe para el consumidor; el llamante decide
|
|
148
|
+
* qué significa (denegar en lectura, 422 en escritura). La raíz no se
|
|
149
|
+
* pregunta: el motor la conoce y su cadena es `[APP_SCOPE]` por definición.
|
|
150
|
+
*
|
|
151
|
+
* La RESPUESTA se valida (D13 + K1): no-array, elemento mal formado
|
|
152
|
+
* (`{app, 'X'}`, `{organization, 'a|b'}`), cadena vacía, o un elemento 0 que
|
|
153
|
+
* no es el scope pedido (otro tipo; un uuid que no es el mismo id salvo
|
|
154
|
+
* mayúsculas/guiones) ⇒ 503 con el motivo como causa. Nunca se acepta en
|
|
155
|
+
* silencio una identidad que el llamante no pidió.
|
|
156
|
+
*/
|
|
157
|
+
export async function resolveChain(resolver, scope, operation) {
|
|
158
|
+
if (scope.type === APP_SCOPE_TYPE)
|
|
159
|
+
return [APP_SCOPE];
|
|
160
|
+
let chain;
|
|
161
|
+
try {
|
|
162
|
+
chain = await resolver(scope);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (isAuthzError(error))
|
|
166
|
+
throw error;
|
|
167
|
+
throw new ScopeResolverError(operation, error);
|
|
168
|
+
}
|
|
169
|
+
if (chain === null || chain === undefined)
|
|
170
|
+
return null;
|
|
171
|
+
if (!Array.isArray(chain)) {
|
|
172
|
+
throw new ScopeResolverError(operation, new TypeError(`resolveChain devolvió ${typeof chain} en vez de ScopeRef[] | null`));
|
|
173
|
+
}
|
|
174
|
+
if (chain.length === 0) {
|
|
175
|
+
throw new ScopeResolverError(operation, new TypeError(`resolveChain devolvió una cadena vacía para ${scope.type}:${scope.uuid ?? ''}: la cadena empieza por el propio scope canónico`));
|
|
176
|
+
}
|
|
177
|
+
for (const element of chain) {
|
|
178
|
+
try {
|
|
179
|
+
assertScope(element);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
throw new ScopeResolverError(operation, error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const self = chain[0];
|
|
186
|
+
if (self.type !== scope.type || !sameUuidLoosely(self.uuid, scope.uuid)) {
|
|
187
|
+
throw new ScopeResolverError(operation, new TypeError(`resolveChain devolvió ${self.type}:${self.uuid ?? ''} como elemento 0 de la cadena de ` +
|
|
188
|
+
`${scope.type}:${scope.uuid ?? ''}: el primer elemento tiene que ser el propio scope (canónico), no otro`));
|
|
189
|
+
}
|
|
190
|
+
return chain;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* El scope CANÓNICO para una escritura que no exige que exista (`revoke`,
|
|
194
|
+
* `removeDeny`, `purgeScope`): la fila del árbol si lo conoce (un alias se
|
|
195
|
+
* canoniza y el hecho canónico se encuentra, K1); el scope tal cual si ya no
|
|
196
|
+
* lo conoce (un hecho de un scope borrado sin avisar, D8, sigue siendo
|
|
197
|
+
* alcanzable con su identidad exacta) o si el driver no tiene resolutor (solo
|
|
198
|
+
* existe la raíz: no hay nada con lo que canonizar). Un resolutor que LANZA
|
|
199
|
+
* sigue siendo 503: no se limpia a ciegas cuando el árbol está caído.
|
|
200
|
+
*/
|
|
201
|
+
export async function canonicalScope(resolver, scope, operation) {
|
|
202
|
+
try {
|
|
203
|
+
const chain = await resolveChain(resolver, scope, operation);
|
|
204
|
+
return chain ? chain[0] : scope;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (error instanceof NoScopeResolverError)
|
|
208
|
+
return scope;
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* **Los scopes destino de un DELETE que no exige que el scope exista**
|
|
214
|
+
* (`revoke`/`removeDeny`; 3b-8 · A4, mismo cierre que 3b-2h · 🟠 3 dio a
|
|
215
|
+
* `scopes.detached`). Con la fila viva es exactamente UNO, el canónico
|
|
216
|
+
* (`chain[0]`). Sin cadena con la que canonizar —la fila ya borrada, o sin
|
|
217
|
+
* resolutor— la ortografía del llamante a secas hacía del delete un no-op
|
|
218
|
+
* SILENCIOSO cuando llegaba un alias del uuid (el mismo uuid sin guiones,
|
|
219
|
+
* que el tipo `uuid` de PG o una collation `*_ci` funden con la fila real):
|
|
220
|
+
* el hecho canónico seguía escrito y volvía a conceder si el scope se
|
|
221
|
+
* restauraba. Se reusa el fan-out de `scopeSpellings`: la ortografía del
|
|
222
|
+
* llamante Y la canónica de la que puede ser alias. Quitar nunca concede,
|
|
223
|
+
* así que borrar de más no existe aquí.
|
|
224
|
+
*/
|
|
225
|
+
export async function canonicalScopeTargets(resolver, scope, operation) {
|
|
226
|
+
try {
|
|
227
|
+
const chain = await resolveChain(resolver, scope, operation);
|
|
228
|
+
if (chain)
|
|
229
|
+
return [chain[0]];
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (!(error instanceof NoScopeResolverError))
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
return scopeSpellings(scope);
|
|
236
|
+
}
|
|
237
|
+
/** La cadena canónica, o 422 `E_AUTHZ_UNKNOWN_SCOPE` si el scope no existe (para escrituras). */
|
|
238
|
+
export async function assertKnownScope(resolver, scope, operation) {
|
|
239
|
+
const chain = await resolveChain(resolver, scope, operation);
|
|
240
|
+
if (!chain) {
|
|
241
|
+
throw new UnknownScopeError(`El scope ${scope.type}:${scope.uuid} no existe para el resolutor de la cadena (${operation}). ` +
|
|
242
|
+
`No se escribe sobre un scope que el árbol no reconoce.`);
|
|
243
|
+
}
|
|
244
|
+
return chain;
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=backend_guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backend_guard.js","sourceRoot":"","sources":["../../../src/shared/backend_guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cómo viaja `expires_at` entre el driver `database` y cada motor SQL
|
|
3
|
+
* (2.5-B · K2, auditor 🟠 2).
|
|
4
|
+
*
|
|
5
|
+
* El problema: en MySQL `expires_at` es `DATETIME(3)`, que NO guarda zona, y
|
|
6
|
+
* `mysql2` serializa y parsea los `Date` con el `TZ` del proceso (`timezone:
|
|
7
|
+
* 'local'`, su default). Un proceso en UTC escribía `12:00:00` para
|
|
8
|
+
* `12:00Z`; uno en Caracas leía ese `12:00:00` como `16:00Z`: la asignación
|
|
9
|
+
* caducaba 4 h tarde para él (y 9 h antes para uno en Tokio). Dentro de un
|
|
10
|
+
* solo proceso el ida y vuelta era exacto, que es lo único que el juez podía
|
|
11
|
+
* ver; el defecto solo aparece con dos procesos, que es el despliegue normal.
|
|
12
|
+
*
|
|
13
|
+
* La decisión: el driver NO depende de la config de la conexión. En MySQL
|
|
14
|
+
* escribe la caducidad como CADENA UTC explícita (`YYYY-MM-DD HH:mm:ss.SSS`),
|
|
15
|
+
* compara con `now` formateado igual y la LEE con `DATE_FORMAT(…)` (una
|
|
16
|
+
* cadena, que se parsea como UTC), así que `timezone`, `dateStrings` o el
|
|
17
|
+
* `TZ` del proceso no intervienen. En PostgreSQL la columna es
|
|
18
|
+
* `timestamptz(3)` (instante absoluto: el `Date` basta) y en SQLite un
|
|
19
|
+
* número; ahí el codec es la identidad. Lo fija `tests/expiry_timezone.spec`
|
|
20
|
+
* con procesos hijos reales en `TZ` distinta sobre la misma base.
|
|
21
|
+
*/
|
|
22
|
+
export interface ExpiryCodec {
|
|
23
|
+
/** Valor con el que se ESCRIBE la columna (`insert`/`update`). */
|
|
24
|
+
toDb(expiresAt: Date | null): unknown;
|
|
25
|
+
/** Valor con el que se COMPARA (`expires_at > ?`) para un instante dado. */
|
|
26
|
+
bind(at: Date): unknown;
|
|
27
|
+
/** Expresión de SELECT que devuelve la columna de forma que `fromDb` la lea sin depender del cliente. */
|
|
28
|
+
select(column: string, alias?: string): unknown;
|
|
29
|
+
/** Lo que devuelve el SELECT de `select(...)` → `Date | null`. */
|
|
30
|
+
fromDb(value: unknown): Date | null;
|
|
31
|
+
}
|
|
32
|
+
/** Nombre del dialecto de una conexión de Lucid (`dialect.name`; fallback: el driver de knex). */
|
|
33
|
+
export declare function dialectOf(connection: any): string;
|
|
34
|
+
/**
|
|
35
|
+
* ¿Es SQLite? (3E · Q5) Lucid nombra su dialecto `better-sqlite3` —no
|
|
36
|
+
* empieza por `sqlite`—, así que un `startsWith('sqlite')` no reconocía el
|
|
37
|
+
* SQLite real de esta máquina ni el de CI. Donde eso decide una BARRERA (el
|
|
38
|
+
* cerrojo del catálogo, que SQLite no necesita porque ya serializa sus
|
|
39
|
+
* escrituras) el fallo es silencioso: solo era inocuo porque knex compila
|
|
40
|
+
* `forUpdate` a vacío en sqlite3.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isSqliteDialect(connection: any): boolean;
|
|
43
|
+
/** `2030-06-15T12:00:00.000Z` → `2030-06-15 12:00:00.000` (UTC, con milisegundos). */
|
|
44
|
+
export declare function formatUtcDatetime(at: Date): string;
|
|
45
|
+
/** `2030-06-15 12:00:00.000000` (UTC, hasta microsegundos) → `Date`, o `null` si no es una fecha. */
|
|
46
|
+
export declare function parseUtcDatetime(value: string): Date | null;
|
|
47
|
+
/**
|
|
48
|
+
* El codec para una conexión de Lucid (default: la primaria). Se decide por
|
|
49
|
+
* dialecto una vez por driver; MySQL es el único que necesita algo distinto
|
|
50
|
+
* de la identidad.
|
|
51
|
+
*/
|
|
52
|
+
export declare function sqlExpiryCodec(connection?: any): ExpiryCodec;
|
|
53
|
+
//# sourceMappingURL=sql_expiry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql_expiry.d.ts","sourceRoot":"","sources":["../../../src/shared/sql_expiry.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,WAAW;IAC1B,kEAAkE;IAClE,IAAI,CAAC,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,OAAO,CAAA;IACrC,4EAA4E;IAC5E,IAAI,CAAC,EAAE,EAAE,IAAI,GAAG,OAAO,CAAA;IACvB,yGAAyG;IACzG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IAC/C,kEAAkE;IAClE,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,GAAG,IAAI,CAAA;CACpC;AAID,kGAAkG;AAClG,wBAAgB,SAAS,CAAC,UAAU,EAAE,GAAG,GAAG,MAAM,CAEjD;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,GAAG,GAAG,OAAO,CAGxD;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,IAAI,GAAG,MAAM,CAElD;AAED,qGAAqG;AACrG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAM3D;AA6BD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,GAAE,GAAqB,GAAG,WAAW,CAE7E"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import db from '@adonisjs/lucid/services/db';
|
|
2
|
+
import { AuthorizationInternalError } from '../errors.js';
|
|
3
|
+
import { toExpiryDate } from '../expiry.js';
|
|
4
|
+
const MYSQL_DIALECTS = new Set(['mysql', 'mysql2']);
|
|
5
|
+
/** Nombre del dialecto de una conexión de Lucid (`dialect.name`; fallback: el driver de knex). */
|
|
6
|
+
export function dialectOf(connection) {
|
|
7
|
+
return connection?.dialect?.name ?? connection?.client?.driverName ?? 'desconocido';
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* ¿Es SQLite? (3E · Q5) Lucid nombra su dialecto `better-sqlite3` —no
|
|
11
|
+
* empieza por `sqlite`—, así que un `startsWith('sqlite')` no reconocía el
|
|
12
|
+
* SQLite real de esta máquina ni el de CI. Donde eso decide una BARRERA (el
|
|
13
|
+
* cerrojo del catálogo, que SQLite no necesita porque ya serializa sus
|
|
14
|
+
* escrituras) el fallo es silencioso: solo era inocuo porque knex compila
|
|
15
|
+
* `forUpdate` a vacío en sqlite3.
|
|
16
|
+
*/
|
|
17
|
+
export function isSqliteDialect(connection) {
|
|
18
|
+
const name = dialectOf(connection);
|
|
19
|
+
return name.startsWith('sqlite') || name === 'better-sqlite3';
|
|
20
|
+
}
|
|
21
|
+
/** `2030-06-15T12:00:00.000Z` → `2030-06-15 12:00:00.000` (UTC, con milisegundos). */
|
|
22
|
+
export function formatUtcDatetime(at) {
|
|
23
|
+
return at.toISOString().slice(0, 23).replace('T', ' ');
|
|
24
|
+
}
|
|
25
|
+
/** `2030-06-15 12:00:00.000000` (UTC, hasta microsegundos) → `Date`, o `null` si no es una fecha. */
|
|
26
|
+
export function parseUtcDatetime(value) {
|
|
27
|
+
const match = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(?:\.(\d{1,6}))?$/.exec(value.trim());
|
|
28
|
+
if (!match)
|
|
29
|
+
return null;
|
|
30
|
+
const millis = (match[3] ?? '').padEnd(3, '0').slice(0, 3);
|
|
31
|
+
const date = new Date(`${match[1]}T${match[2]}.${millis}Z`);
|
|
32
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
33
|
+
}
|
|
34
|
+
const identityCodec = {
|
|
35
|
+
toDb: (expiresAt) => expiresAt,
|
|
36
|
+
bind: (at) => at,
|
|
37
|
+
select: (column, alias) => (alias && alias !== column ? `${column} as ${alias}` : column),
|
|
38
|
+
fromDb: toExpiryDate,
|
|
39
|
+
};
|
|
40
|
+
function mysqlCodec(connection) {
|
|
41
|
+
const raw = (sql, bindings) => typeof connection?.raw === 'function' ? connection.raw(sql, bindings) : db.raw(sql, bindings);
|
|
42
|
+
return {
|
|
43
|
+
toDb: (expiresAt) => (expiresAt ? formatUtcDatetime(expiresAt) : null),
|
|
44
|
+
bind: (at) => formatUtcDatetime(at),
|
|
45
|
+
select: (column, alias) => raw(`DATE_FORMAT(??, '%Y-%m-%d %H:%i:%s.%f') as ??`, [column, alias ?? column.split('.').pop()]),
|
|
46
|
+
fromDb: (value) => {
|
|
47
|
+
if (value === null || value === undefined || value === '')
|
|
48
|
+
return null;
|
|
49
|
+
if (typeof value === 'string')
|
|
50
|
+
return parseUtcDatetime(value);
|
|
51
|
+
// Un `Date` aquí es que la lectura no pasó por `select(...)` y el
|
|
52
|
+
// cliente ya la interpretó con SU zona: eso es exactamente lo que este
|
|
53
|
+
// codec existe para impedir. Ruidoso, nunca una caducidad corrida.
|
|
54
|
+
throw new AuthorizationInternalError(`expires_at llegó como ${value instanceof Date ? 'Date' : typeof value} en MySQL: la lectura tiene que pasar por ExpiryCodec.select(...) (DATE_FORMAT) para no depender de la zona del cliente`);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* El codec para una conexión de Lucid (default: la primaria). Se decide por
|
|
60
|
+
* dialecto una vez por driver; MySQL es el único que necesita algo distinto
|
|
61
|
+
* de la identidad.
|
|
62
|
+
*/
|
|
63
|
+
export function sqlExpiryCodec(connection = db.connection()) {
|
|
64
|
+
return MYSQL_DIALECTS.has(dialectOf(connection)) ? mysqlCodec(connection) : identityCodec;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=sql_expiry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql_expiry.js","sourceRoot":"","sources":["../../../src/shared/sql_expiry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ScopeDescendantsResolver, ScopeEdgesEnumerator } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* `descendantsOf` opt-in sobre una tabla del consumidor con columna padre
|
|
4
|
+
* (2.1, B2). Genera UNA CTE recursiva (`WITH RECURSIVE`), la misma para
|
|
5
|
+
* PostgreSQL, SQLite y MySQL 8 (2.5 · J3: solo cambia el carácter con el que
|
|
6
|
+
* se citan los identificadores); cualquier otro dialecto lanza 500
|
|
7
|
+
* `E_AUTHZ_UNSUPPORTED_DIALECT` en la primera llamada hasta que la suite lo
|
|
8
|
+
* observe. El paquete sigue sin conocer el árbol del consumidor: aquí solo
|
|
9
|
+
* se le dice qué tabla y qué columnas lo guardan.
|
|
10
|
+
*
|
|
11
|
+
* scopes: {
|
|
12
|
+
* resolveChain,
|
|
13
|
+
* descendantsOf: sqlDescendantsOf({
|
|
14
|
+
* table: 'organization_nodes', uuidColumn: 'uuid', parentColumn: 'parent_uuid', typeColumn: 'kind',
|
|
15
|
+
* }),
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* Cotas: la consulta lee como mucho `maxNodes + 1` filas (`LIMIT`) y no baja
|
|
19
|
+
* más de `maxNodes + 1` niveles, así que un ciclo en la tabla termina Y se
|
|
20
|
+
* nota: un ciclo alcanzable produce al menos una fila por nivel, es decir
|
|
21
|
+
* más de `maxNodes` ⇒ 422 `E_AUTHZ_TOO_MANY_SCOPES` con «posible ciclo» en
|
|
22
|
+
* el mensaje (con `depth < maxNodes` un ciclo de dos nodos devolvía
|
|
23
|
+
* `maxNodes - 1` filas duplicadas en silencio). No hay segunda barrera por
|
|
24
|
+
* uuid repetido: con esta profundidad un ciclo NUNCA cabe en `maxNodes`
|
|
25
|
+
* filas, así que era código muerto (G1, CR6). Nunca una lista parcial. Un
|
|
26
|
+
* scope que no está en la tabla ⇒ `null`. `scopeType` pasa por la gramática
|
|
27
|
+
* de identidad al construir (auditor 11).
|
|
28
|
+
*/
|
|
29
|
+
export interface SqlDescendantsOptions {
|
|
30
|
+
table: string;
|
|
31
|
+
uuidColumn: string;
|
|
32
|
+
/** Columna con el uuid del padre; `NULL` = el nodo cuelga de `app`. */
|
|
33
|
+
parentColumn: string;
|
|
34
|
+
/** Columna con el `ScopeRef.type` de cada fila. Excluyente con `scopeType`. */
|
|
35
|
+
typeColumn?: string;
|
|
36
|
+
/** Tipo fijo de todos los nodos de la tabla (sin columna de tipo). Excluyente con `typeColumn`. */
|
|
37
|
+
scopeType?: string;
|
|
38
|
+
/** Cota propia del helper: se aplica `min(maxNodes del llamante, esta)`. */
|
|
39
|
+
maxNodes?: number;
|
|
40
|
+
/** Nombre de la conexión Lucid (default: la primaria). */
|
|
41
|
+
connection?: string;
|
|
42
|
+
/** Deadline de cada consulta en ms (default 5000): vencido ⇒ 503. */
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
}
|
|
45
|
+
/** Lo que necesita del `db` de Lucid; inyectable para probar el dialecto sin servidor. */
|
|
46
|
+
interface DatabaseLike {
|
|
47
|
+
connection(name?: string): any;
|
|
48
|
+
}
|
|
49
|
+
export declare function sqlDescendantsOf(options: SqlDescendantsOptions, database?: DatabaseLike): ScopeDescendantsResolver;
|
|
50
|
+
/**
|
|
51
|
+
* `scopes.enumerateEdges` sobre la MISMA tabla que `sqlDescendantsOf`
|
|
52
|
+
* (3b-3a): una página de aristas `hijo → padre` por llamada, ordenadas por la
|
|
53
|
+
* clave primaria y continuadas con un cursor.
|
|
54
|
+
*
|
|
55
|
+
* scopes: {
|
|
56
|
+
* resolveChain,
|
|
57
|
+
* enumerateEdges: sqlScopeEdges({
|
|
58
|
+
* table: 'organization_nodes', uuidColumn: 'uuid',
|
|
59
|
+
* parentColumn: 'parent_uuid', typeColumn: 'kind',
|
|
60
|
+
* }),
|
|
61
|
+
* }
|
|
62
|
+
*
|
|
63
|
+
* Lo usa `authz:reconcile --to=openfga` para reconstruir el árbol del store
|
|
64
|
+
* (y para borrar las aristas que la tabla ya no respalda).
|
|
65
|
+
*
|
|
66
|
+
* Decisiones que hay que conocer:
|
|
67
|
+
* - **el cursor es el uuid de la última fila**, y el orden es `ORDER BY
|
|
68
|
+
* uuid` (clave primaria: orden total y estable). Una pasada reanudada no
|
|
69
|
+
* se salta nodos aunque el árbol cambie entre páginas; lo que cambie por
|
|
70
|
+
* debajo del cursor entra en la siguiente pasada, que es idempotente.
|
|
71
|
+
* - **una fila cuyo padre NO existe no produce arista** (LEFT JOIN sin
|
|
72
|
+
* pareja). Es coherente con `resolveChain`, que devuelve `null` para ese
|
|
73
|
+
* nodo: no está en el árbol. No es silencioso: el destino conserva la
|
|
74
|
+
* arista vieja, `authz:reconcile` la cuenta como sobrante y sus hechos
|
|
75
|
+
* salen en `skipped` con el motivo.
|
|
76
|
+
* - `parentColumn IS NULL` es la raíz (`APP_SCOPE`).
|
|
77
|
+
*
|
|
78
|
+
* Mismos dialectos que `sqlDescendantsOf` (PG, MySQL 8, SQLite) y el mismo
|
|
79
|
+
* 500 `E_AUTHZ_UNSUPPORTED_DIALECT` para cualquier otro.
|
|
80
|
+
*/
|
|
81
|
+
export interface SqlScopeEdgesOptions {
|
|
82
|
+
table: string;
|
|
83
|
+
uuidColumn: string;
|
|
84
|
+
/** Columna con el uuid del padre; `NULL` = el nodo cuelga de `app`. */
|
|
85
|
+
parentColumn: string;
|
|
86
|
+
/** Columna con el `ScopeRef.type` de cada fila. Excluyente con `scopeType`. */
|
|
87
|
+
typeColumn?: string;
|
|
88
|
+
/** Tipo fijo de todos los nodos de la tabla (sin columna de tipo). Excluyente con `typeColumn`. */
|
|
89
|
+
scopeType?: string;
|
|
90
|
+
/** Nombre de la conexión Lucid (default: la primaria). */
|
|
91
|
+
connection?: string;
|
|
92
|
+
/** Deadline de cada consulta en ms (default 5000): vencido ⇒ 503. */
|
|
93
|
+
timeoutMs?: number;
|
|
94
|
+
}
|
|
95
|
+
export declare function sqlScopeEdges(options: SqlScopeEdgesOptions, database?: DatabaseLike): ScopeEdgesEnumerator;
|
|
96
|
+
export {};
|
|
97
|
+
//# sourceMappingURL=sql_descendants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql_descendants.d.ts","sourceRoot":"","sources":["../../src/sql_descendants.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,wBAAwB,EAAa,oBAAoB,EAAY,MAAM,YAAY,CAAA;AAErG;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,uEAAuE;IACvE,YAAY,EAAE,MAAM,CAAA;IACpB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mGAAmG;IACnG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAuBD,0FAA0F;AAC1F,UAAU,YAAY;IACpB,UAAU,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,GAAG,CAAA;CAC/B;AAED,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,qBAAqB,EAC9B,QAAQ,GAAE,YAAiB,GAC1B,wBAAwB,CAiH1B;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,uEAAuE;IACvE,YAAY,EAAE,MAAM,CAAA;IACpB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,mGAAmG;IACnG,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,GAAE,YAAiB,GAC1B,oBAAoB,CA+EtB"}
|