snake-orm 0.1.0b1__py3-none-any.whl
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.
- snake_orm-0.1.0b1.dist-info/METADATA +366 -0
- snake_orm-0.1.0b1.dist-info/RECORD +198 -0
- snake_orm-0.1.0b1.dist-info/WHEEL +4 -0
- snake_orm-0.1.0b1.dist-info/entry_points.txt +2 -0
- snake_orm-0.1.0b1.dist-info/licenses/LICENSE +21 -0
- snakeorm/__init__.py +214 -0
- snakeorm/advisor.py +109 -0
- snakeorm/cli/__init__.py +3 -0
- snakeorm/cli/app.py +992 -0
- snakeorm/cli/discovery.py +179 -0
- snakeorm/cli/hooks.py +86 -0
- snakeorm/compiler/__init__.py +3 -0
- snakeorm/compiler/compiler.py +330 -0
- snakeorm/connection.py +228 -0
- snakeorm/contrib/__init__.py +17 -0
- snakeorm/contrib/asgi.py +192 -0
- snakeorm/contrib/config.py +264 -0
- snakeorm/contrib/deliver.py +256 -0
- snakeorm/contrib/django.py +245 -0
- snakeorm/contrib/sidecar.py +44 -0
- snakeorm/contrib/wsgi.py +176 -0
- snakeorm/core/__init__.py +1 -0
- snakeorm/core/config.py +173 -0
- snakeorm/core/converters.py +172 -0
- snakeorm/core/exceptions.py +175 -0
- snakeorm/core/placement.py +15 -0
- snakeorm/core/sentinels.py +37 -0
- snakeorm/core/signals.py +114 -0
- snakeorm/debug/__init__.py +42 -0
- snakeorm/debug/assets/css/panel.css +616 -0
- snakeorm/debug/assets/css/snake_orm_app.css +293 -0
- snakeorm/debug/assets/img/icon.svg +5 -0
- snakeorm/debug/assets/js/history.js +425 -0
- snakeorm/debug/assets/js/language.js +283 -0
- snakeorm/debug/assets/js/panel.js +241 -0
- snakeorm/debug/assets/js/snake_orm_app.js +82 -0
- snakeorm/debug/assets/pages/cli.html +28 -0
- snakeorm/debug/assets/pages/config.html +34 -0
- snakeorm/debug/assets/pages/database_first.html +29 -0
- snakeorm/debug/assets/pages/help.html +21 -0
- snakeorm/debug/assets/pages/history.html +27 -0
- snakeorm/debug/capture.py +234 -0
- snakeorm/debug/channel.py +131 -0
- snakeorm/debug/collector.py +144 -0
- snakeorm/debug/config.py +149 -0
- snakeorm/debug/html.py +578 -0
- snakeorm/debug/matrix.py +60 -0
- snakeorm/debug/origin.py +42 -0
- snakeorm/debug/otel/__init__.py +68 -0
- snakeorm/debug/otel/context.py +43 -0
- snakeorm/debug/otel/exporter.py +277 -0
- snakeorm/debug/otel/payload.py +97 -0
- snakeorm/debug/otel/report.py +29 -0
- snakeorm/debug/otel/spans.py +393 -0
- snakeorm/debug/otel/summary.py +59 -0
- snakeorm/debug/record.py +54 -0
- snakeorm/debug/report.py +311 -0
- snakeorm/debug/testing.py +23 -0
- snakeorm/decorators/__init__.py +13 -0
- snakeorm/decorators/abstract.py +26 -0
- snakeorm/decorators/db_first.py +104 -0
- snakeorm/decorators/function.py +28 -0
- snakeorm/decorators/model.py +268 -0
- snakeorm/decorators/polymorphic.py +140 -0
- snakeorm/decorators/result.py +167 -0
- snakeorm/decorators/row.py +90 -0
- snakeorm/decorators/trigger.py +47 -0
- snakeorm/decorators/view.py +149 -0
- snakeorm/dialects/__init__.py +6 -0
- snakeorm/dialects/base.py +373 -0
- snakeorm/dialects/capabilities.py +571 -0
- snakeorm/dialects/literals.py +46 -0
- snakeorm/dialects/matrix.py +384 -0
- snakeorm/dialects/mysql.py +519 -0
- snakeorm/dialects/postgres.py +504 -0
- snakeorm/dialects/sqlite.py +417 -0
- snakeorm/drivers/__init__.py +18 -0
- snakeorm/drivers/asyncbase.py +73 -0
- snakeorm/drivers/asyncdecorators.py +244 -0
- snakeorm/drivers/asyncpool.py +228 -0
- snakeorm/drivers/asyncpsycopg.py +124 -0
- snakeorm/drivers/asyncpymysql.py +50 -0
- snakeorm/drivers/asyncsqlite.py +30 -0
- snakeorm/drivers/base.py +76 -0
- snakeorm/drivers/failures.py +181 -0
- snakeorm/drivers/logging.py +167 -0
- snakeorm/drivers/pool.py +352 -0
- snakeorm/drivers/psycopg.py +188 -0
- snakeorm/drivers/pymysql.py +176 -0
- snakeorm/drivers/savepoints.py +46 -0
- snakeorm/drivers/sqlite.py +170 -0
- snakeorm/drivers/threaded.py +143 -0
- snakeorm/drivers/timeout.py +87 -0
- snakeorm/dto/__init__.py +55 -0
- snakeorm/dto/read.py +231 -0
- snakeorm/dto/region.py +326 -0
- snakeorm/dto/resolve.py +354 -0
- snakeorm/dto/spec.py +201 -0
- snakeorm/expressions/__init__.py +88 -0
- snakeorm/expressions/conditional.py +178 -0
- snakeorm/expressions/expression.py +810 -0
- snakeorm/expressions/functions.py +71 -0
- snakeorm/expressions/keys.py +299 -0
- snakeorm/expressions/paths.py +43 -0
- snakeorm/expressions/scalar.py +293 -0
- snakeorm/expressions/window.py +248 -0
- snakeorm/fields/__init__.py +68 -0
- snakeorm/fields/check.py +79 -0
- snakeorm/fields/column.py +316 -0
- snakeorm/fields/enum.py +64 -0
- snakeorm/fields/index.py +46 -0
- snakeorm/fields/relationship.py +738 -0
- snakeorm/fields/typed.py +736 -0
- snakeorm/helpers/__init__.py +1 -0
- snakeorm/helpers/annotations.py +21 -0
- snakeorm/helpers/inheritance.py +41 -0
- snakeorm/helpers/pyliteral.py +52 -0
- snakeorm/helpers/pytype.py +50 -0
- snakeorm/introspection/__init__.py +13 -0
- snakeorm/introspection/base.py +30 -0
- snakeorm/introspection/drift.py +78 -0
- snakeorm/introspection/models.py +633 -0
- snakeorm/introspection/mysql.py +367 -0
- snakeorm/introspection/postgres.py +397 -0
- snakeorm/introspection/sqlite.py +217 -0
- snakeorm/introspection/unsupported.py +94 -0
- snakeorm/linker/__init__.py +3 -0
- snakeorm/linker/linker.py +521 -0
- snakeorm/metadata/__init__.py +41 -0
- snakeorm/metadata/check.py +37 -0
- snakeorm/metadata/column.py +201 -0
- snakeorm/metadata/enum_storage.py +33 -0
- snakeorm/metadata/fk_action.py +19 -0
- snakeorm/metadata/foreign_key.py +26 -0
- snakeorm/metadata/index.py +43 -0
- snakeorm/metadata/index_method.py +19 -0
- snakeorm/metadata/int_size.py +20 -0
- snakeorm/metadata/json_storage.py +21 -0
- snakeorm/metadata/polymorphic.py +30 -0
- snakeorm/metadata/primary_key.py +23 -0
- snakeorm/metadata/relationship.py +60 -0
- snakeorm/metadata/relationship_kind.py +43 -0
- snakeorm/metadata/routine.py +26 -0
- snakeorm/metadata/server_default.py +20 -0
- snakeorm/metadata/table.py +97 -0
- snakeorm/metadata/table_kind.py +23 -0
- snakeorm/metadata/trigger.py +62 -0
- snakeorm/metadata/type_params.py +348 -0
- snakeorm/migration/__init__.py +88 -0
- snakeorm/migration/asyncrunner.py +168 -0
- snakeorm/migration/autodetect.py +96 -0
- snakeorm/migration/ddl.py +1172 -0
- snakeorm/migration/diff.py +701 -0
- snakeorm/migration/loader.py +77 -0
- snakeorm/migration/operations.py +952 -0
- snakeorm/migration/planner.py +78 -0
- snakeorm/migration/realize.py +420 -0
- snakeorm/migration/renames.py +153 -0
- snakeorm/migration/render.py +1095 -0
- snakeorm/migration/runner.py +314 -0
- snakeorm/migration/squash.py +60 -0
- snakeorm/migration/state.py +107 -0
- snakeorm/model.py +162 -0
- snakeorm/py.typed +0 -0
- snakeorm/query/__init__.py +9 -0
- snakeorm/query/compound.py +288 -0
- snakeorm/query/join_kind.py +19 -0
- snakeorm/query/joined.py +143 -0
- snakeorm/query/query.py +1001 -0
- snakeorm/query/recursive.py +244 -0
- snakeorm/registry/__init__.py +5 -0
- snakeorm/registry/by_module.py +163 -0
- snakeorm/registry/registry.py +166 -0
- snakeorm/session/__init__.py +8 -0
- snakeorm/session/asyncsession.py +638 -0
- snakeorm/session/coercion.py +306 -0
- snakeorm/session/factory.py +29 -0
- snakeorm/session/guards.py +269 -0
- snakeorm/session/isolation.py +23 -0
- snakeorm/session/mapper.py +213 -0
- snakeorm/session/planning.py +815 -0
- snakeorm/session/retry.py +96 -0
- snakeorm/session/session.py +871 -0
- snakeorm/session/shared.py +355 -0
- snakeorm/sql/__init__.py +18 -0
- snakeorm/sql/adapt.py +104 -0
- snakeorm/sql/aggregate.py +232 -0
- snakeorm/sql/condition.py +359 -0
- snakeorm/sql/delete.py +46 -0
- snakeorm/sql/insert.py +149 -0
- snakeorm/sql/joins.py +158 -0
- snakeorm/sql/pk_subquery.py +54 -0
- snakeorm/sql/refs.py +19 -0
- snakeorm/sql/resolver.py +28 -0
- snakeorm/sql/select.py +175 -0
- snakeorm/sql/update.py +82 -0
- snakeorm/sql/value.py +552 -0
- snakeorm/times.py +276 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LANGUAGE module (`SnakeOrmLanguage`): the ES/EN TEXTS of everything rendered + the language
|
|
3
|
+
* get/set. No i18n library: it holds the strings and swaps them. Self-contained (only it touches
|
|
4
|
+
* its sessionStorage key); the orchestrator asks it for `t(key)` and sends it `apply`/`set`.
|
|
5
|
+
*/
|
|
6
|
+
const SnakeOrmLanguage = (() => {
|
|
7
|
+
/** Valid languages (single source; exposed so the orchestrator never uses bare 'es'/'en'). @enum {string} */
|
|
8
|
+
const LANG = { ES: 'es', EN: 'en' };
|
|
9
|
+
/** Each language's name IN ITSELF (endonym), for the `<select>` options. @type {Record<string,string>} */
|
|
10
|
+
const NAMES = { [LANG.ES]: 'Español', [LANG.EN]: 'English' };
|
|
11
|
+
/** `sessionStorage` key for the language (only the user's CHOICE is stored, not the default). */
|
|
12
|
+
const STORE_KEY = 'snakeorm-debug-lang';
|
|
13
|
+
/** Language used when nothing is stored (public tool: English). */
|
|
14
|
+
const DEFAULT = LANG.EN;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* ES/EN texts. The server renders ES by default in the HTML marked with `data-t` (text),
|
|
18
|
+
* `data-tt` (title) and `data-ta` (aria-label); `apply` replaces them. @type {Record<string, Record<string, string>>}
|
|
19
|
+
*/
|
|
20
|
+
const STRINGS = {
|
|
21
|
+
[LANG.ES]: {
|
|
22
|
+
queries: 'queries', req: 'petición', db: 'en BD', map: 'mapeo', app: 'en app',
|
|
23
|
+
dups: 'duplicadas', slowest: 'más lenta',
|
|
24
|
+
queries_tip: 'Nº de sentencias SQL que corrió esta petición.',
|
|
25
|
+
req_tip:
|
|
26
|
+
'Tiempo total del request en el servidor, de principio a fin. No incluye el viaje de red ' +
|
|
27
|
+
'hasta tu navegador.',
|
|
28
|
+
db_tip:
|
|
29
|
+
'Lo que la app esperó al driver, no lo que el motor tardó en ejecutar: incluye el viaje ' +
|
|
30
|
+
'de red a la BD.',
|
|
31
|
+
map_tip: 'Convertir filas en objetos: el coste del ORM. Tu código no está aquí dentro.',
|
|
32
|
+
app_tip: 'El resto = petición − BD − mapeo: tu Python y la plantilla.',
|
|
33
|
+
dups_tip: 'Misma SQL y misma línea corriendo más de una vez (posible N+1 o trabajo repetido).',
|
|
34
|
+
slowest_tip: 'Duración de la query más lenta de esta petición.',
|
|
35
|
+
collapse: 'Colapsar todo', expand: 'Expandir todo',
|
|
36
|
+
per_page: 'Queries por página', per_page_all: 'Todas',
|
|
37
|
+
empty: 'Sin queries', rows: 'filas devueltas/afectadas', origin_in: 'en',
|
|
38
|
+
theme: 'Tema', theme_aria: 'Cambiar tema', close: 'Cerrar', lang: 'Idioma',
|
|
39
|
+
prev: 'Anterior', next: 'Siguiente',
|
|
40
|
+
warns_title: 'posibles N+1',
|
|
41
|
+
warn_pre: 'La misma SQL corrió', warn_post: 'veces (posible N+1):',
|
|
42
|
+
hint_title: 'Índices sugeridos', hint_suf: 'sin índice; una query lenta filtró aquí',
|
|
43
|
+
menu_queries: 'Consultas', menu_history: 'Historial', menu_help: 'Ayuda',
|
|
44
|
+
menu_config: 'Config', menu_cli: 'CLI', menu_dbfirst: 'DB-first',
|
|
45
|
+
hcalls: 'llamadas',
|
|
46
|
+
hcalls_tip: 'Llamadas que la página ha hecho desde que cargó.',
|
|
47
|
+
hqueries_tip: 'Suma de las consultas de esas llamadas.',
|
|
48
|
+
hdb_tip: 'Suma de lo que esas llamadas esperaron a la BD.',
|
|
49
|
+
hmap_tip: 'Suma del mapeo de esas llamadas: filas convertidas en objetos.',
|
|
50
|
+
hslowest_tip: 'La llamada que más esperó a la BD de toda la lista.',
|
|
51
|
+
hpart_tip:
|
|
52
|
+
'Calculado solo sobre las llamadas que trajeron el dato; las demás no cuentan como cero.',
|
|
53
|
+
hist_title: 'Llamadas posteriores',
|
|
54
|
+
hist_intro:
|
|
55
|
+
'El informe de arriba es el de la petición que pintó esta página. Aquí se apilan las que ' +
|
|
56
|
+
'vienen después, sin recargar.',
|
|
57
|
+
hist_empty: 'Aún no hay llamadas.',
|
|
58
|
+
hist_off: 'Falta el canal envelope',
|
|
59
|
+
hist_off_d:
|
|
60
|
+
'El histórico lee el informe que el ORM cuelga de las respuestas JSON. Sin ese canal no ' +
|
|
61
|
+
'llega ninguno, así que aquí no habría nada que apilar:',
|
|
62
|
+
hist_off_why:
|
|
63
|
+
'El panel lo dice y no lo enciende: ssr y envelope son canales independientes y quien los ' +
|
|
64
|
+
'elige eres tú.',
|
|
65
|
+
hist_warnings: 'Avisos', hist_loading: 'Pidiendo el informe…',
|
|
66
|
+
hist_gone: 'Ese informe ya no está: el buffer del sidecar solo guarda los más recientes.',
|
|
67
|
+
hist_failed: 'No se pudo pedir el informe de esta llamada.',
|
|
68
|
+
hist_none: 'Sin detalle: esta llamada solo trajo cabeceras. Enciende el canal sidecar.',
|
|
69
|
+
badge_tip:
|
|
70
|
+
'Consultas desde que cargó la página: las de esta petición más las de las llamadas ' +
|
|
71
|
+
'posteriores, que verás en Historial.',
|
|
72
|
+
help_enable: 'Activar el panel', help_json: 'JSON de la API',
|
|
73
|
+
help_json_d: 'Con el canal envelope, en cada respuesta JSON bajo la clave', help_shortcuts: 'Atajos',
|
|
74
|
+
help_shortcuts_d: 'Esc cierra · arrastra el botón para moverlo · ◐ tema · 🌐 idioma',
|
|
75
|
+
help_read: 'Leer las consultas',
|
|
76
|
+
help_read_d:
|
|
77
|
+
'×N = misma consulta repetida · ↳ fichero:línea = de dónde salió · los ? ya llevan su valor ' +
|
|
78
|
+
'real · pliega los avisos y elige cuántas queries ves por página para ganar sitio',
|
|
79
|
+
cli_migrations: 'Migraciones', cli_make: 'crear migración', cli_migrate: 'aplicar',
|
|
80
|
+
cli_rollback: 'deshacer la última', cli_status: 'aplicadas / pendientes',
|
|
81
|
+
cli_fresh: 'recrear la BD desde cero', cli_squash: 'colapsar el histórico',
|
|
82
|
+
cli_check: 'código contra la BD real', cli_generate: 'Generar',
|
|
83
|
+
cli_scaffold: 'modelos desde una BD existente', cli_dto: 'TypedDicts desde tus modelos',
|
|
84
|
+
cli_inspect: 'Inspección', cli_tables: 'listar tablas (--detail, --from-db)',
|
|
85
|
+
cli_table: 'detalle de una tabla', cli_perf: 'Rendimiento',
|
|
86
|
+
cli_advise: 'FKs sin índice (auditoría estática)',
|
|
87
|
+
cfg_channels: 'Canales de debug',
|
|
88
|
+
cfg_channels_d: 'Qué entrega el panel. Se combinan por coma:',
|
|
89
|
+
cfg_ch_ssr: 'panel HTML inyectado en la respuesta',
|
|
90
|
+
cfg_ch_envelope: 'el debug en el JSON, bajo la clave',
|
|
91
|
+
cfg_ch_timing: 'cabecera Server-Timing',
|
|
92
|
+
cfg_ch_sidecar: 'un token y su propia página en',
|
|
93
|
+
cfg_ch_otel: 'DECLARADO y sin implementar: encenderlo no entrega nada y avisa',
|
|
94
|
+
cfg_advise: 'Umbral del asesor',
|
|
95
|
+
cfg_advise_d:
|
|
96
|
+
'Solo sugiere índices para queries más lentas que X ms (por debajo, un índice no cambia nada):',
|
|
97
|
+
cfg_advise_code: 'O tipado en código:', cfg_prod: 'Producción',
|
|
98
|
+
cfg_prod_env:
|
|
99
|
+
'Del entorno no se adivina nada: si hay un canal de riesgo encendido y nadie lo ha '
|
|
100
|
+
+ 'declarado —ni esta variable, ni production= en el middleware, ni SnakeDebugConfig— el '
|
|
101
|
+
+ 'arranque se niega en vez de elegir un defecto.',
|
|
102
|
+
cfg_prod_d:
|
|
103
|
+
'Los que entregan el SQL a quien pide —ssr, envelope y sidecar— se caen con production=True, aunque los configures. timing se queda: mide, no cuenta.',
|
|
104
|
+
dbf_scaffold: 'Generar modelos', dbf_scaffold_d: 'Espeja una BD existente a modelos Python:',
|
|
105
|
+
dbf_scaffold_u: 'Re-sincronizar (sobrescribe entero):', dbf_mirror: 'Modelo espejo',
|
|
106
|
+
dbf_scaffold_out: 'Sin', dbf_scaffold_out2: 'no genera: no hay ruta por defecto y falla diciéndolo.',
|
|
107
|
+
dbf_mirror_d:
|
|
108
|
+
'marca un modelo como espejo: las migraciones lo IGNORAN. El esquema es del sysadmin, no nuestro.',
|
|
109
|
+
dbf_mirror_adopt:
|
|
110
|
+
'NO hay adopción in situ: cambiarlo a @snake_model no le entrega los mandos a las migraciones, porque el histórico no conoce la tabla y el autogen solo emitiría un CreateTable, que contra la tabla existente muere. Sirve para LLEVARSE el esquema a otra base gobernada desde cero.',
|
|
111
|
+
dbf_names: 'Nombres de las clases',
|
|
112
|
+
dbf_names_d: 'La clase se llama como la tabla, en CapWords y con el esquema delante:',
|
|
113
|
+
dbf_names_u: 'deja los guiones bajos',
|
|
114
|
+
dbf_names_s: 'quita el esquema; dos esquemas con la misma tabla COLISIONAN y se avisa',
|
|
115
|
+
dbf_names_why:
|
|
116
|
+
'No se quita el plural: eso es adivinar en inglés y rompe status, analysis o direcciones. Lo que no se puede nombrar se avisa y se deja fuera, nunca se inventa.',
|
|
117
|
+
cfg_where: 'Qué va en config y qué en el comando',
|
|
118
|
+
cfg_where_d: 'La regla: si la aplicación lo necesita para funcionar, va en config; si solo lo necesita esa invocación, va en flag.',
|
|
119
|
+
cfg_where_cfg: 'databases, debug, advise_ms, migrations_dir, language',
|
|
120
|
+
cfg_where_cli: 'Flags del comando:',
|
|
121
|
+
cfg_where_db: '--database no sobrescribe nada: ELIGE una de las conexiones que declara la config. Un nombre que no esté no se puede crear desde el comando.',
|
|
122
|
+
cfg_where_django: 'En Django no se declara aparte: el CLI sube hasta manage.py, lee su DJANGO_SETTINGS_MODULE y traduce settings.DATABASES + settings.SNAKEORM al mismo objeto.',
|
|
123
|
+
dbf_check: 'Deriva',
|
|
124
|
+
dbf_check_d: 'Compara el código con la BD real y avisa si no cuadran:',
|
|
125
|
+
},
|
|
126
|
+
[LANG.EN]: {
|
|
127
|
+
queries: 'queries', req: 'request', db: 'in DB', map: 'mapping', app: 'in app',
|
|
128
|
+
dups: 'duplicates', slowest: 'slowest',
|
|
129
|
+
queries_tip: 'Number of SQL statements this request ran.',
|
|
130
|
+
req_tip:
|
|
131
|
+
'Total server-side request time, end to end. It does not include the network trip to ' +
|
|
132
|
+
'your browser.',
|
|
133
|
+
db_tip:
|
|
134
|
+
'What the app WAITED on the driver, not what the engine took to run: the trip to the DB ' +
|
|
135
|
+
'is in here.',
|
|
136
|
+
map_tip: 'Turning rows into objects: the ORM cost. Your own code is not in here.',
|
|
137
|
+
app_tip: 'The rest = request − DB − mapping: your Python and the template.',
|
|
138
|
+
dups_tip: 'The same SQL from the same line, running more than once (possible N+1 or repeated work).',
|
|
139
|
+
slowest_tip: 'Duration of the slowest query in this request.',
|
|
140
|
+
collapse: 'Collapse all', expand: 'Expand all',
|
|
141
|
+
per_page: 'Queries per page', per_page_all: 'All',
|
|
142
|
+
empty: 'No queries', rows: 'rows returned/affected', origin_in: 'in',
|
|
143
|
+
theme: 'Theme', theme_aria: 'Toggle theme', close: 'Close', lang: 'Language',
|
|
144
|
+
prev: 'Previous', next: 'Next',
|
|
145
|
+
warns_title: 'possible N+1',
|
|
146
|
+
warn_pre: 'The same SQL ran', warn_post: 'times (possible N+1):',
|
|
147
|
+
hint_title: 'Suggested indexes', hint_suf: 'no index; a slow query filtered here',
|
|
148
|
+
menu_queries: 'Queries', menu_history: 'History', menu_help: 'Help',
|
|
149
|
+
menu_config: 'Config', menu_cli: 'CLI', menu_dbfirst: 'DB-first',
|
|
150
|
+
hcalls: 'calls',
|
|
151
|
+
hcalls_tip: 'Calls the page has made since it loaded.',
|
|
152
|
+
hqueries_tip: 'Sum of the queries of those calls.',
|
|
153
|
+
hdb_tip: 'Sum of what those calls waited on the DB.',
|
|
154
|
+
hmap_tip: 'Sum of the mapping of those calls: rows turned into objects.',
|
|
155
|
+
hslowest_tip: 'The call that waited longest on the DB, of the whole list.',
|
|
156
|
+
hpart_tip:
|
|
157
|
+
'Computed only over the calls that reported the number; the rest do not count as zero.',
|
|
158
|
+
hist_title: 'Calls after the render',
|
|
159
|
+
hist_intro:
|
|
160
|
+
'The report above is the one for the request that painted this page. The ones that come ' +
|
|
161
|
+
'later, without a reload, stack up here.',
|
|
162
|
+
hist_empty: 'No calls yet.',
|
|
163
|
+
hist_off: 'The envelope channel is off',
|
|
164
|
+
hist_off_d:
|
|
165
|
+
'The history reads the report the ORM hangs off JSON responses. Without that channel none ' +
|
|
166
|
+
'arrives, so there would be nothing to stack here:',
|
|
167
|
+
hist_off_why:
|
|
168
|
+
'The panel says so and does not switch it on: ssr and envelope are independent channels, ' +
|
|
169
|
+
'and choosing them is yours.',
|
|
170
|
+
hist_warnings: 'Warnings', hist_loading: 'Asking for the report…',
|
|
171
|
+
hist_gone: 'That report is gone: the sidecar buffer only keeps the most recent ones.',
|
|
172
|
+
hist_failed: 'The report for this call could not be fetched.',
|
|
173
|
+
hist_none: 'No detail: this call only brought headers. Switch the sidecar channel on.',
|
|
174
|
+
badge_tip:
|
|
175
|
+
'Queries since the page loaded: this request plus the calls that came after it, which you ' +
|
|
176
|
+
'can read in History.',
|
|
177
|
+
help_enable: 'Enable the panel', help_json: 'API JSON',
|
|
178
|
+
help_json_d: 'With the envelope channel, in every JSON response under the key', help_shortcuts: 'Shortcuts',
|
|
179
|
+
help_shortcuts_d: 'Esc closes · drag the button to move it · ◐ theme · 🌐 language',
|
|
180
|
+
help_read: 'Reading queries',
|
|
181
|
+
help_read_d:
|
|
182
|
+
'×N = same query repeated · ↳ file:line = where it came from · the ? already show their real ' +
|
|
183
|
+
'value · fold the warnings and pick how many queries per page to gain room',
|
|
184
|
+
cli_migrations: 'Migrations', cli_make: 'create migration', cli_migrate: 'apply',
|
|
185
|
+
cli_rollback: 'undo last', cli_status: 'applied / pending',
|
|
186
|
+
cli_fresh: 'rebuild the DB from scratch', cli_squash: 'squash history',
|
|
187
|
+
cli_check: 'code against the real DB', cli_generate: 'Generate',
|
|
188
|
+
cli_scaffold: 'models from an existing DB', cli_dto: 'TypedDicts from your models',
|
|
189
|
+
cli_inspect: 'Inspection', cli_tables: 'list tables (--detail, --from-db)',
|
|
190
|
+
cli_table: 'one table in detail', cli_perf: 'Performance',
|
|
191
|
+
cli_advise: 'unindexed FKs (static audit)',
|
|
192
|
+
cfg_channels: 'Debug channels',
|
|
193
|
+
cfg_channels_d: 'What the panel delivers. They combine, comma-separated:',
|
|
194
|
+
cfg_ch_ssr: 'HTML panel injected into the response',
|
|
195
|
+
cfg_ch_envelope: 'the debug in the JSON, under the key',
|
|
196
|
+
cfg_ch_timing: 'Server-Timing header',
|
|
197
|
+
cfg_ch_sidecar: 'a token and its own page at',
|
|
198
|
+
cfg_ch_otel: 'DECLARED and not implemented: switching it on delivers nothing and warns',
|
|
199
|
+
cfg_advise: 'Advisor threshold',
|
|
200
|
+
cfg_advise_d:
|
|
201
|
+
'Only suggests indexes for queries slower than X ms (below that, an index changes nothing):',
|
|
202
|
+
cfg_advise_code: 'Or typed in code:', cfg_prod: 'Production',
|
|
203
|
+
cfg_prod_env:
|
|
204
|
+
'Nothing about the environment is guessed: with a risky channel on and nobody having '
|
|
205
|
+
+ 'declared it —not this variable, not production= on the middleware, not '
|
|
206
|
+
+ 'SnakeDebugConfig— startup refuses rather than picking a default.',
|
|
207
|
+
cfg_prod_d:
|
|
208
|
+
'The ones that hand the SQL to the requester —ssr, envelope and sidecar— are dropped with production=True, even if configured. timing stays: it measures, it does not tell.',
|
|
209
|
+
dbf_scaffold: 'Generate models', dbf_scaffold_d: 'Mirror an existing DB to Python models:',
|
|
210
|
+
dbf_scaffold_u: 'Re-sync (overwrites entirely):', dbf_mirror: 'Mirror model',
|
|
211
|
+
dbf_scaffold_out: 'Without', dbf_scaffold_out2: 'it generates nothing: there is no default path and it fails saying so.',
|
|
212
|
+
dbf_mirror_d:
|
|
213
|
+
'marks a model as a mirror: migrations IGNORE it. The schema belongs to the sysadmin, not to us.',
|
|
214
|
+
dbf_mirror_adopt:
|
|
215
|
+
'There is NO in-place adoption: switching it to @snake_model does not hand the controls to the migrations, because the history does not know the table and the autogen would only emit a CreateTable, which dies against the existing one. It is for TAKING the schema to another database governed from scratch.',
|
|
216
|
+
dbf_names: 'Class names',
|
|
217
|
+
dbf_names_d: 'The class is named after the table, CapWords, with the schema in front:',
|
|
218
|
+
dbf_names_u: 'keeps the underscores',
|
|
219
|
+
dbf_names_s: 'drops the schema; two schemas with the same table then COLLIDE, and it is reported',
|
|
220
|
+
dbf_names_why:
|
|
221
|
+
'The plural is left alone: removing it guesses at English and breaks status, analysis or direcciones. What cannot be named is reported and left out, never invented.',
|
|
222
|
+
cfg_where: 'What goes in config and what in the command',
|
|
223
|
+
cfg_where_d: 'The rule: if the application needs it to run, it goes in config; if only that invocation needs it, it is a flag.',
|
|
224
|
+
cfg_where_cfg: 'databases, debug, advise_ms, migrations_dir, language',
|
|
225
|
+
cfg_where_cli: 'Command flags:',
|
|
226
|
+
cfg_where_db: '--database overrides nothing: it PICKS one of the connections the config declares. A name that is not there cannot be created from the command.',
|
|
227
|
+
cfg_where_django: 'Django declares none of this twice: the CLI walks up to manage.py, reads its DJANGO_SETTINGS_MODULE and translates settings.DATABASES + settings.SNAKEORM into the same object.',
|
|
228
|
+
dbf_check: 'Drift',
|
|
229
|
+
dbf_check_d: 'Compares the code against the real DB and warns if they diverge:',
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/** Active language. @type {string} */
|
|
234
|
+
let current = DEFAULT;
|
|
235
|
+
|
|
236
|
+
const safe = (fn) => {
|
|
237
|
+
try {
|
|
238
|
+
return fn();
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/** Active language. */
|
|
245
|
+
const get = () => current;
|
|
246
|
+
/** Translated text for `key` in the active language. */
|
|
247
|
+
const t = (key) => STRINGS[current][key];
|
|
248
|
+
/** The stored choice, or `null` if the user never chose. */
|
|
249
|
+
const stored = () => safe(() => sessionStorage.getItem(STORE_KEY));
|
|
250
|
+
|
|
251
|
+
/** Swaps texts (`data-t`), tooltips (`data-tt`) and aria (`data-ta`) in `root` to the active language. */
|
|
252
|
+
const swap = (root) => {
|
|
253
|
+
const dict = STRINGS[current];
|
|
254
|
+
root.querySelectorAll('[data-t]').forEach((el) => {
|
|
255
|
+
if (dict[el.dataset.t] != null) el.textContent = dict[el.dataset.t];
|
|
256
|
+
});
|
|
257
|
+
root.querySelectorAll('[data-tt]').forEach((el) => {
|
|
258
|
+
if (dict[el.dataset.tt] != null) el.setAttribute('title', dict[el.dataset.tt]);
|
|
259
|
+
});
|
|
260
|
+
root.querySelectorAll('[data-ta]').forEach((el) => {
|
|
261
|
+
if (dict[el.dataset.ta] != null) el.setAttribute('aria-label', dict[el.dataset.ta]);
|
|
262
|
+
});
|
|
263
|
+
// The <select> reflects the active language (its selected option).
|
|
264
|
+
const select = root.querySelector('.snk-lang');
|
|
265
|
+
if (select) select.value = current;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/** Applies a language to `root` WITHOUT persisting (for startup: it does not dirty the storage). */
|
|
269
|
+
const apply = (root, lang) => {
|
|
270
|
+
current = STRINGS[lang] ? lang : DEFAULT;
|
|
271
|
+
swap(root);
|
|
272
|
+
return current;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/** Changes the language and PERSISTS it (for the user's click). */
|
|
276
|
+
const set = (root, lang) => {
|
|
277
|
+
apply(root, lang);
|
|
278
|
+
safe(() => sessionStorage.setItem(STORE_KEY, current));
|
|
279
|
+
return current;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
return { LANG, NAMES, DEFAULT, get, t, stored, apply, set };
|
|
283
|
+
})();
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug panel — pure UI (`SnakeOrmPanel`). A catalogue of FUNCTIONS over the Shadow DOM; it knows
|
|
3
|
+
* nothing about the language and never touches `window`. `mount` mounts and wires the intrinsic bits
|
|
4
|
+
* (draggable FAB, per-card collapse, pager); the rest are operations the ORCHESTRATOR wires to the controls.
|
|
5
|
+
*/
|
|
6
|
+
const SnakeOrmPanel = (() => {
|
|
7
|
+
/** Id of the (light DOM) container where `html.py` injects the panel; the orchestrator looks it up here. */
|
|
8
|
+
const HOST_ID = 'snk-root';
|
|
9
|
+
|
|
10
|
+
/** Class names (they match the CSS and `html.py`). @enum {string} */
|
|
11
|
+
const CLASS = {
|
|
12
|
+
root: 'snk', fab: 'snk-fab', panel: 'snk-panel', backdrop: 'snk-back',
|
|
13
|
+
close: 'snk-close', theme: 'snk-theme',
|
|
14
|
+
lang: 'snk-lang', menuItem: 'snk-menu-item', view: 'snk-view', active: 'snk-active',
|
|
15
|
+
badge: 'snk-badge', hot: 'snk-hot',
|
|
16
|
+
toggleAll: 'snk-all', pager: 'snk-pager', pageLabel: 'snk-pglabel', pageBtn: 'snk-pg', ps: 'snk-ps',
|
|
17
|
+
query: 'snk-q', queryHead: 'snk-qh', light: 'snk-light', dragging: 'snk-drag',
|
|
18
|
+
open: 'snk-open', show: 'snk-show', expanded: 'snk-exp', pagerActive: 'snk-on', hidden: 'snk-hidden',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** The panel's own `sessionStorage` keys (the language is handled by its module). @enum {string} */
|
|
22
|
+
const STORE = { position: 'snakeorm-debug-fab', theme: 'snakeorm-debug-theme' };
|
|
23
|
+
|
|
24
|
+
/** `data-*` names (camelCase in `dataset`). `mounted` marks the host as already mounted. @enum {string} */
|
|
25
|
+
const DATA = { pageSize: 'ps', direction: 'd', view: 'view', mounted: 'snkInit' };
|
|
26
|
+
|
|
27
|
+
/** Pager directions (value of `data-d`). @enum {number} */
|
|
28
|
+
const STEP = { prev: -1, next: 1 };
|
|
29
|
+
|
|
30
|
+
/** Px threshold that tells a CLICK from a FAB drag. */
|
|
31
|
+
const CLICK_SLOP = 6;
|
|
32
|
+
/** Minimum margin from the FAB to the viewport edge, in px. */
|
|
33
|
+
const EDGE = 8;
|
|
34
|
+
|
|
35
|
+
// State (filled in by `mount`).
|
|
36
|
+
/** @type {ShadowRoot | HTMLElement} */ let shadow;
|
|
37
|
+
/** @type {HTMLElement} */ let wrap;
|
|
38
|
+
/** @type {HTMLElement} */ let fab;
|
|
39
|
+
/** @type {HTMLElement} */ let panel;
|
|
40
|
+
/** @type {HTMLElement} */ let backdrop;
|
|
41
|
+
/** @type {HTMLElement[]} */ let cards = [];
|
|
42
|
+
let pageSize = 25;
|
|
43
|
+
let pages = 1;
|
|
44
|
+
let page = 0;
|
|
45
|
+
let dragging = false;
|
|
46
|
+
let startX = 0;
|
|
47
|
+
let startY = 0;
|
|
48
|
+
let grabX = 0;
|
|
49
|
+
let grabY = 0;
|
|
50
|
+
|
|
51
|
+
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
52
|
+
|
|
53
|
+
/** Runs `fn` swallowing exceptions (sessionStorage fails in private mode). @template T */
|
|
54
|
+
const safe = (fn) => {
|
|
55
|
+
try {
|
|
56
|
+
return fn();
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const one = (cls) => shadow.querySelector(`.${cls}`);
|
|
63
|
+
const many = (cls) => [...shadow.querySelectorAll(`.${cls}`)];
|
|
64
|
+
const pageBtn = (dir) => shadow.querySelector(`.${CLASS.pageBtn}[data-${DATA.direction}="${dir}"]`);
|
|
65
|
+
|
|
66
|
+
/** Places the FAB at (x, y), clamped to the viewport. */
|
|
67
|
+
const place = (x, y) => {
|
|
68
|
+
const { offsetWidth: w, offsetHeight: h } = fab;
|
|
69
|
+
Object.assign(fab.style, {
|
|
70
|
+
left: `${clamp(x, EDGE, innerWidth - w - EDGE)}px`,
|
|
71
|
+
top: `${clamp(y, EDGE, innerHeight - h - EDGE)}px`,
|
|
72
|
+
right: 'auto',
|
|
73
|
+
bottom: 'auto',
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Opens/closes the offcanvas and hides the FAB when opening. */
|
|
78
|
+
const setOpen = (show) => {
|
|
79
|
+
panel.classList.toggle(CLASS.open, show);
|
|
80
|
+
backdrop.classList.toggle(CLASS.show, show);
|
|
81
|
+
fab.classList.toggle(CLASS.hidden, show);
|
|
82
|
+
panel.setAttribute('aria-hidden', String(!show));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Shows only the current page's queries and updates the pager. */
|
|
86
|
+
const paint = () => {
|
|
87
|
+
cards.forEach((card, i) => {
|
|
88
|
+
card.style.display = i >= page * pageSize && i < (page + 1) * pageSize ? '' : 'none';
|
|
89
|
+
});
|
|
90
|
+
const label = one(CLASS.pageLabel);
|
|
91
|
+
if (label) label.textContent = `${page + 1} / ${pages}`;
|
|
92
|
+
const prev = pageBtn(STEP.prev);
|
|
93
|
+
const next = pageBtn(STEP.next);
|
|
94
|
+
if (prev) prev.disabled = page === 0;
|
|
95
|
+
if (next) next.disabled = page >= pages - 1;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Wires the draggable FAB (position persisted; small click = toggle). */
|
|
99
|
+
const wireDrag = () => {
|
|
100
|
+
const saved = safe(() => JSON.parse(sessionStorage.getItem(STORE.position)));
|
|
101
|
+
if (saved) place(saved.x, saved.y);
|
|
102
|
+
fab.addEventListener('pointerdown', (e) => {
|
|
103
|
+
dragging = true;
|
|
104
|
+
const r = fab.getBoundingClientRect();
|
|
105
|
+
startX = e.clientX;
|
|
106
|
+
startY = e.clientY;
|
|
107
|
+
grabX = e.clientX - r.left;
|
|
108
|
+
grabY = e.clientY - r.top;
|
|
109
|
+
fab.setPointerCapture(e.pointerId);
|
|
110
|
+
fab.classList.add(CLASS.dragging);
|
|
111
|
+
});
|
|
112
|
+
fab.addEventListener('pointermove', (e) => {
|
|
113
|
+
if (dragging) place(e.clientX - grabX, e.clientY - grabY);
|
|
114
|
+
});
|
|
115
|
+
fab.addEventListener('pointerup', (e) => {
|
|
116
|
+
if (!dragging) return;
|
|
117
|
+
dragging = false;
|
|
118
|
+
fab.classList.remove(CLASS.dragging);
|
|
119
|
+
const r = fab.getBoundingClientRect();
|
|
120
|
+
safe(() => sessionStorage.setItem(STORE.position, JSON.stringify({ x: r.left, y: r.top })));
|
|
121
|
+
if (Math.hypot(e.clientX - startX, e.clientY - startY) < CLICK_SLOP) toggle();
|
|
122
|
+
});
|
|
123
|
+
addEventListener('resize', () => {
|
|
124
|
+
const r = fab.getBoundingClientRect();
|
|
125
|
+
place(r.left, r.top);
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** Shows or hides the pager as needed (more than one page at the current size). */
|
|
130
|
+
const updatePager = () => {
|
|
131
|
+
const pager = one(CLASS.pager);
|
|
132
|
+
if (pager) pager.classList.toggle(CLASS.pagerActive, pages > 1);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Changes how many queries show per page (0 = all), recomputes pages and repaints from the 1st. */
|
|
136
|
+
const setPageSize = (n) => {
|
|
137
|
+
pageSize = n > 0 ? n : Math.max(cards.length, 1);
|
|
138
|
+
pages = Math.max(1, Math.ceil(cards.length / pageSize));
|
|
139
|
+
page = 0;
|
|
140
|
+
updatePager();
|
|
141
|
+
paint();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Wires each card's own collapse and the pager (the buttons, ALWAYS). */
|
|
145
|
+
const wireList = () => {
|
|
146
|
+
cards.forEach((card) => {
|
|
147
|
+
card
|
|
148
|
+
.querySelector(`.${CLASS.queryHead}`)
|
|
149
|
+
.addEventListener('click', () => card.classList.toggle(CLASS.expanded));
|
|
150
|
+
});
|
|
151
|
+
many(CLASS.pageBtn).forEach((btn) =>
|
|
152
|
+
btn.addEventListener('click', () => {
|
|
153
|
+
page = clamp(page + Number(btn.dataset[DATA.direction]), 0, pages - 1);
|
|
154
|
+
paint();
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
updatePager();
|
|
158
|
+
paint();
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// ===== Operations it exposes (the orchestrator wires them to the controls) ====================
|
|
162
|
+
|
|
163
|
+
/** What the FAB badge currently says, as a number (0 when there is no badge to read). */
|
|
164
|
+
const badgeCount = () => Number(one(CLASS.badge)?.textContent) || 0;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Writes the FAB badge, and lights it amber when asked. It never puts the light OUT: the render
|
|
168
|
+
* report may have turned it on for its own duplicates, and that fact does not stop being true
|
|
169
|
+
* because a later call came back clean.
|
|
170
|
+
*/
|
|
171
|
+
const badge = (count, hot) => {
|
|
172
|
+
const element = one(CLASS.badge);
|
|
173
|
+
if (!element) return;
|
|
174
|
+
element.textContent = String(count);
|
|
175
|
+
if (hot) element.classList.add(CLASS.hot);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const open = () => setOpen(true);
|
|
179
|
+
const close = () => setOpen(false);
|
|
180
|
+
const toggle = () => setOpen(!panel.classList.contains(CLASS.open));
|
|
181
|
+
|
|
182
|
+
/** Toggles light/dark theme and persists it. */
|
|
183
|
+
const theme = () => {
|
|
184
|
+
const light = wrap.classList.toggle(CLASS.light);
|
|
185
|
+
safe(() => sessionStorage.setItem(STORE.theme, light ? 'light' : 'dark'));
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** Button text key for the current state: 'expand' if any card is collapsed, otherwise 'collapse'. */
|
|
189
|
+
const collapseKey = () =>
|
|
190
|
+
cards.some((card) => !card.classList.contains(CLASS.expanded)) ? 'expand' : 'collapse';
|
|
191
|
+
|
|
192
|
+
/** Expands or collapses ALL the cards. */
|
|
193
|
+
const toggleAll = () => {
|
|
194
|
+
const expand = collapseKey() === 'expand';
|
|
195
|
+
cards.forEach((card) => card.classList.toggle(CLASS.expanded, expand));
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/** Sets the "collapse all" button text for the current state, using the injected `translate`. */
|
|
199
|
+
const syncCollapse = (translate) => {
|
|
200
|
+
const button = one(CLASS.toggleAll);
|
|
201
|
+
if (button) button.textContent = translate(collapseKey());
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/** Activates the menu's `name` view (Queries/Help) and highlights its item. */
|
|
205
|
+
const showView = (name) => {
|
|
206
|
+
many(CLASS.menuItem).forEach((item) =>
|
|
207
|
+
item.classList.toggle(CLASS.active, item.dataset[DATA.view] === name),
|
|
208
|
+
);
|
|
209
|
+
many(CLASS.view).forEach((view) =>
|
|
210
|
+
view.classList.toggle(CLASS.active, view.classList.contains(`${CLASS.view}-${name}`)),
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Mounts the panel on `host`: Shadow DOM + refs + wires the intrinsic bits (drag, cards, pager).
|
|
216
|
+
* Returns the root the panel lives in (so the orchestrator can wire controls and language).
|
|
217
|
+
* @param {HTMLElement} host @returns {ShadowRoot | HTMLElement}
|
|
218
|
+
*/
|
|
219
|
+
const mount = (host) => {
|
|
220
|
+
shadow = host.attachShadow ? host.attachShadow({ mode: 'open' }) : host;
|
|
221
|
+
shadow.appendChild(host.querySelector('template').content.cloneNode(true));
|
|
222
|
+
pageSize = Number(host.dataset[DATA.pageSize]) || 25;
|
|
223
|
+
wrap = one(CLASS.root);
|
|
224
|
+
fab = one(CLASS.fab);
|
|
225
|
+
panel = one(CLASS.panel);
|
|
226
|
+
backdrop = one(CLASS.backdrop);
|
|
227
|
+
cards = many(CLASS.query);
|
|
228
|
+
pages = Math.max(1, Math.ceil(cards.length / pageSize));
|
|
229
|
+
if (safe(() => sessionStorage.getItem(STORE.theme)) === 'light') wrap.classList.add(CLASS.light);
|
|
230
|
+
wireDrag();
|
|
231
|
+
wireList();
|
|
232
|
+
return shadow;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// It also exposes its vocabularies (`HOST_ID`/`CLASS`/`DATA`) so the orchestrator wires without
|
|
236
|
+
// literals, and `safe` so the other modules share ONE swallow-everything helper.
|
|
237
|
+
return {
|
|
238
|
+
HOST_ID, CLASS, DATA, safe, mount, open, close, toggle, theme,
|
|
239
|
+
toggleAll, syncCollapse, showView, setPageSize, badge, badgeCount,
|
|
240
|
+
};
|
|
241
|
+
})();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/// <reference path="./language.js" />
|
|
2
|
+
/// <reference path="./panel.js" />
|
|
3
|
+
/// <reference path="./history.js" />
|
|
4
|
+
/**
|
|
5
|
+
* ORCHESTRATOR (`snake_orm_app`) — the controller and owner of the SINGLETON. It takes the host, mounts
|
|
6
|
+
* the panel and WIRES every control to the modules' functions (panel = UI, language = texts,
|
|
7
|
+
* history = the page's later calls), using the VOCABULARIES each module exposes (no loose literals
|
|
8
|
+
* here). It registers the public API on `window.snakeOrmDebug`. No runtime `import`: the modules are
|
|
9
|
+
* concatenated into one inlined `<script type="module">` and share scope (the `///` lines are just
|
|
10
|
+
* for IntelliSense).
|
|
11
|
+
*/
|
|
12
|
+
const snakeOrmDebug = (() => {
|
|
13
|
+
const { HOST_ID, CLASS, DATA } = SnakeOrmPanel;
|
|
14
|
+
|
|
15
|
+
const host = document.getElementById(HOST_ID);
|
|
16
|
+
if (!host || host.dataset[DATA.mounted]) return null; // no panel, or already mounted
|
|
17
|
+
host.dataset[DATA.mounted] = '1';
|
|
18
|
+
|
|
19
|
+
const root = SnakeOrmPanel.mount(host);
|
|
20
|
+
const t = SnakeOrmLanguage.t;
|
|
21
|
+
const on = (cls, fn) => root.querySelector(`.${cls}`)?.addEventListener('click', fn);
|
|
22
|
+
|
|
23
|
+
// Controls → the modules' functions.
|
|
24
|
+
on(CLASS.close, SnakeOrmPanel.close);
|
|
25
|
+
on(CLASS.backdrop, SnakeOrmPanel.close);
|
|
26
|
+
addEventListener('keydown', (e) => e.key === 'Escape' && SnakeOrmPanel.close());
|
|
27
|
+
on(CLASS.theme, SnakeOrmPanel.theme);
|
|
28
|
+
on(CLASS.toggleAll, () => {
|
|
29
|
+
SnakeOrmPanel.toggleAll();
|
|
30
|
+
SnakeOrmPanel.syncCollapse(t);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Queries-per-page selector (25 / 50 / all): on change, it recomputes and repaints.
|
|
34
|
+
const psSelect = root.querySelector(`.${CLASS.ps}`);
|
|
35
|
+
if (psSelect) {
|
|
36
|
+
psSelect.addEventListener('change', () => SnakeOrmPanel.setPageSize(Number(psSelect.value)));
|
|
37
|
+
}
|
|
38
|
+
root.querySelectorAll(`.${CLASS.menuItem}`).forEach((item) =>
|
|
39
|
+
item.addEventListener('click', () => SnakeOrmPanel.showView(item.dataset[DATA.view])),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Language selector (<select>): options come from the module, and picking one switches+persists it.
|
|
43
|
+
const langSelect = root.querySelector(`.${CLASS.lang}`);
|
|
44
|
+
if (langSelect) {
|
|
45
|
+
langSelect.innerHTML = Object.entries(SnakeOrmLanguage.NAMES)
|
|
46
|
+
.map(([value, label]) => `<option value="${value}">${label}</option>`)
|
|
47
|
+
.join('');
|
|
48
|
+
langSelect.addEventListener('change', () => {
|
|
49
|
+
SnakeOrmLanguage.set(root, langSelect.value);
|
|
50
|
+
SnakeOrmPanel.syncCollapse(t);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Initial language (not persisted): what the user saved, else the one the server sets in the
|
|
55
|
+
// `data-lang` of #snk-root (the panel config), else the module's default.
|
|
56
|
+
SnakeOrmLanguage.apply(
|
|
57
|
+
root,
|
|
58
|
+
SnakeOrmLanguage.stored() || host.dataset.lang || SnakeOrmLanguage.DEFAULT,
|
|
59
|
+
);
|
|
60
|
+
SnakeOrmPanel.syncCollapse(t);
|
|
61
|
+
|
|
62
|
+
// The history tab: it watches `fetch`/`XMLHttpRequest` and stacks the calls the page makes after
|
|
63
|
+
// the render. It is mounted LAST and only now, with the panel already up: nothing of the host
|
|
64
|
+
// page gets patched until there is a tab to paint into. The badge is the panel's, so the history
|
|
65
|
+
// asks for it instead of writing it — from here on it counts the queries since the page loaded.
|
|
66
|
+
SnakeOrmHistory.mount(root, {
|
|
67
|
+
translate: t,
|
|
68
|
+
queries: SnakeOrmPanel.badgeCount(),
|
|
69
|
+
onCount: SnakeOrmPanel.badge,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
open: SnakeOrmPanel.open,
|
|
74
|
+
close: SnakeOrmPanel.close,
|
|
75
|
+
toggle: SnakeOrmPanel.toggle,
|
|
76
|
+
language: SnakeOrmLanguage,
|
|
77
|
+
};
|
|
78
|
+
})();
|
|
79
|
+
|
|
80
|
+
if (snakeOrmDebug) window.snakeOrmDebug = snakeOrmDebug;
|
|
81
|
+
|
|
82
|
+
export default snakeOrmDebug;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!-- Vista "CLI": los comandos del ORM (`snakeorm ...`), promovidos aquí desde la Ayuda. Estático. Los
|
|
2
|
+
textos traducibles llevan `data-t`; los comandos NO se traducen. Se inlinea como una vista más. -->
|
|
3
|
+
<div class="snk-help">
|
|
4
|
+
<div class="snk-help-card">
|
|
5
|
+
<h4 data-t="cli_migrations">Migraciones</h4>
|
|
6
|
+
<p><code>snakeorm makemigrations</code> · <span data-t="cli_make">crear migración</span></p>
|
|
7
|
+
<p><code>snakeorm migrate</code> · <span data-t="cli_migrate">aplicar</span></p>
|
|
8
|
+
<p><code>snakeorm rollback</code> · <span data-t="cli_rollback">deshacer la última</span></p>
|
|
9
|
+
<p><code>snakeorm status</code> · <span data-t="cli_status">aplicadas / pendientes</span></p>
|
|
10
|
+
<p><code>snakeorm fresh</code> · <span data-t="cli_fresh">recrear la BD desde cero</span></p>
|
|
11
|
+
<p><code>snakeorm squash</code> · <span data-t="cli_squash">colapsar el histórico</span></p>
|
|
12
|
+
<p><code>snakeorm check</code> · <span data-t="cli_check">código contra la BD real</span></p>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="snk-help-card">
|
|
15
|
+
<h4 data-t="cli_inspect">Inspección</h4>
|
|
16
|
+
<p><code>snakeorm tables</code> · <span data-t="cli_tables">listar tablas (--detail, --from-db)</span></p>
|
|
17
|
+
<p><code>snakeorm table <t></code> · <span data-t="cli_table">detalle de una tabla</span></p>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="snk-help-card">
|
|
20
|
+
<h4 data-t="cli_generate">Generar</h4>
|
|
21
|
+
<p><code>snakeorm scaffold</code> · <span data-t="cli_scaffold">modelos desde una BD existente</span></p>
|
|
22
|
+
<p><code>snakeorm dto --sync</code> · <span data-t="cli_dto">TypedDicts desde tus modelos</span></p>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="snk-help-card">
|
|
25
|
+
<h4 data-t="cli_perf">Rendimiento</h4>
|
|
26
|
+
<p><code>snakeorm advise</code> · <span data-t="cli_advise">FKs sin índice (auditoría estática)</span></p>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|