letopis 1.4.1 → 2.0.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/AGENT-CHEATSHEET.md +329 -278
- package/CHANGELOG.md +558 -1
- package/README.md +606 -1037
- package/dist/acl.d.ts +78 -1
- package/dist/acl.js +264 -13
- package/dist/admin.d.ts +10 -141
- package/dist/admin.js +24 -175
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +5 -15
- package/dist/cache.d.ts +8 -1
- package/dist/cache.js +33 -10
- package/dist/chain.d.ts +19 -41
- package/dist/chain.js +169 -201
- package/dist/cli.js +51 -88
- package/dist/demo/booking.d.ts +352 -36
- package/dist/describe.d.ts +47 -0
- package/dist/describe.js +195 -0
- package/dist/errors.d.ts +59 -13
- package/dist/errors.js +190 -29
- package/dist/index.d.ts +52 -71
- package/dist/index.js +245 -130
- package/dist/internal.d.ts +12 -0
- package/dist/internal.js +5 -0
- package/dist/jsonschema.js +18 -17
- package/dist/load.d.ts +20 -12
- package/dist/load.js +225 -186
- package/dist/mcp.d.ts +5 -7
- package/dist/mcp.js +171 -215
- package/dist/messages/access.d.ts +90 -0
- package/dist/messages/access.js +113 -0
- package/dist/messages/api.d.ts +171 -0
- package/dist/messages/api.js +215 -0
- package/dist/messages/boot.d.ts +201 -0
- package/dist/messages/boot.js +256 -0
- package/dist/messages/codes.d.ts +172 -0
- package/dist/messages/codes.js +173 -0
- package/dist/messages/index.d.ts +95 -0
- package/dist/messages/index.js +118 -0
- package/dist/messages/mcp.d.ts +306 -0
- package/dist/messages/mcp.js +387 -0
- package/dist/messages/model.d.ts +233 -0
- package/dist/messages/model.js +283 -0
- package/dist/messages/query.d.ts +131 -0
- package/dist/messages/query.js +158 -0
- package/dist/messages/sql-access.d.ts +70 -0
- package/dist/messages/sql-access.js +87 -0
- package/dist/messages/sql-core.d.ts +150 -0
- package/dist/messages/sql-core.js +181 -0
- package/dist/messages/sql-time.d.ts +71 -0
- package/dist/messages/sql-time.js +88 -0
- package/dist/messages/sql-write.d.ts +246 -0
- package/dist/messages/sql-write.js +270 -0
- package/dist/messages/time.d.ts +23 -0
- package/dist/messages/time.js +29 -0
- package/dist/messages/types.d.ts +27 -0
- package/dist/messages/types.js +5 -0
- package/dist/messages/write.d.ts +405 -0
- package/dist/messages/write.js +509 -0
- package/dist/model.d.ts +73 -60
- package/dist/model.js +26 -29
- package/dist/ops.d.ts +35 -0
- package/dist/ops.js +101 -0
- package/dist/pglite.d.ts +0 -9
- package/dist/pglite.js +3 -1
- package/dist/registry.d.ts +3 -19
- package/dist/registry.js +3 -16
- package/dist/signals.d.ts +5 -9
- package/dist/signals.js +5 -9
- package/dist/sql.d.ts +13 -6
- package/dist/sql.js +176 -83
- package/dist/sync.d.ts +9 -3
- package/dist/sync.js +51 -21
- package/dist/tx.d.ts +26 -1
- package/dist/tx.js +44 -14
- package/dist/typed.d.ts +240 -45
- package/dist/types.d.ts +23 -91
- package/dist/types.js +12 -14
- package/dist/up.d.ts +53 -32
- package/dist/up.js +184 -73
- package/dist/uuid.js +5 -5
- package/dist/validate.d.ts +13 -1
- package/dist/validate.js +61 -29
- package/dist/watch.d.ts +11 -10
- package/dist/watch.js +33 -70
- package/dist/write.d.ts +9 -16
- package/dist/write.js +118 -187
- package/llms.txt +22 -25
- package/package.json +8 -11
- package/sql/10-core.sql +7 -50
- package/sql/15-errors.sql +64 -10
- package/sql/20-context.sql +7 -6
- package/sql/30-validate.sql +35 -27
- package/sql/40-class.sql +20 -114
- package/sql/50-acl.sql +8 -13
- package/sql/60-write.sql +220 -667
- package/sql/70-read.sql +20 -16
- package/sql/80-auth.sql +24 -27
- package/sql/90-time.sql +70 -859
- package/sql/95-seed.system.sql +7 -14
- package/sql/99-revision.sql +2 -1
- package/sql/README.md +32 -24
- package/sql/migrate/README.md +14 -0
- package/AGENT-CHEATSHEET.en.md +0 -485
- package/MIGRATION.md +0 -190
- package/README.en.md +0 -2859
- package/dist/import.d.ts +0 -185
- package/dist/import.js +0 -820
- package/dist/project.d.ts +0 -350
- package/dist/project.js +0 -1224
- package/sql/75-history.sql +0 -358
- package/sql/88-derived.sql +0 -1235
- package/sql/migrate/0006-changed-paths.sql +0 -3
- package/sql/migrate/0007-secret-account-fk.sql +0 -12
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
export const codes = {
|
|
2
|
+
'codes.no_session': {
|
|
3
|
+
code: 'no_session',
|
|
4
|
+
ru: 'нет действующей сессии',
|
|
5
|
+
hint: 'войти заново: истёкшую или отозванную сессию не вернуть',
|
|
6
|
+
},
|
|
7
|
+
'codes.target_not_found': {
|
|
8
|
+
code: 'target_not_found',
|
|
9
|
+
ru: 'цель не найдена — одинаково для несуществующей, чужой и невидимой',
|
|
10
|
+
hint: 'проверить id и права: чужой и невидимый объект неотличим от несуществующего',
|
|
11
|
+
},
|
|
12
|
+
'codes.invalid_data': {
|
|
13
|
+
code: 'invalid_data',
|
|
14
|
+
ru: 'данные не подходят к описанию класса; подробности — в `issues`',
|
|
15
|
+
hint: 'исправить данные по `issues` (путь, правило, сообщение); проверить до отправки — `compileValidator`',
|
|
16
|
+
},
|
|
17
|
+
'codes.undeclared_end': {
|
|
18
|
+
code: 'undeclared_end',
|
|
19
|
+
ru: 'у класса нет такого конца (ссылки)',
|
|
20
|
+
hint: 'объявить конец в модели класса (`ends`) или исправить имя роли',
|
|
21
|
+
},
|
|
22
|
+
'codes.tenant_denied': {
|
|
23
|
+
code: 'tenant_denied',
|
|
24
|
+
ru: 'чужой арендатор или нет членства в нём',
|
|
25
|
+
hint: 'работать в арендаторе, где у пользователя есть членство, — сменить его `auth.switch()`',
|
|
26
|
+
},
|
|
27
|
+
'codes.acl_denied': {
|
|
28
|
+
code: 'acl_denied',
|
|
29
|
+
ru: 'правила доступа запрещают это действие',
|
|
30
|
+
hint: 'добавить правило доступа (`Rule`) или выполнить действие от пользователя, которому оно разрешено; проверить заранее — `db.acl.checkData`, `db.acl.check`',
|
|
31
|
+
},
|
|
32
|
+
'codes.id_mismatch': {
|
|
33
|
+
code: 'id_mismatch',
|
|
34
|
+
ru: 'переданный id не совпадает с id, вычисленным из ключа',
|
|
35
|
+
hint: 'не передавать `id` у класса с ключом — база вычислит его из ключа; узнать заранее — `db.idOf(класс, ключ)`',
|
|
36
|
+
},
|
|
37
|
+
'codes.id_from_db': {
|
|
38
|
+
code: 'id_from_db',
|
|
39
|
+
ru: 'у класса без ключа id выдаёт база — свой id передавать нельзя',
|
|
40
|
+
hint: 'не передавать `id` — база выдаст его сама; нужен постоянный id — дать классу ключ',
|
|
41
|
+
},
|
|
42
|
+
'codes.immutable_key': {
|
|
43
|
+
code: 'immutable_key',
|
|
44
|
+
ru: 'ключевые поля, класс, id, арендатора и владельца правкой не изменить',
|
|
45
|
+
hint: 'ключ, класс, id, арендатор и владелец строки неизменны: объект с другим ключом или классом — новый объект (`create`)',
|
|
46
|
+
},
|
|
47
|
+
'codes.invalid_class': {
|
|
48
|
+
code: 'invalid_class',
|
|
49
|
+
ru: 'описание класса отклонено, класс не найден или абстрактный, либо удаляют класс, у которого есть строки, потомки или история',
|
|
50
|
+
hint: 'проверить имя (`db.registry.has`) и описание класса (`syncModels` без `apply` покажет план); удалить класс можно только без строк, потомков и истории',
|
|
51
|
+
},
|
|
52
|
+
'codes.tightening_conflict': {
|
|
53
|
+
code: 'tightening_conflict',
|
|
54
|
+
ru: 'ужесточению модели мешают строки, которые ей не подходят',
|
|
55
|
+
hint: 'исправить или удалить строки из списка ошибки и снова вызвать `syncModels`',
|
|
56
|
+
},
|
|
57
|
+
'codes.isolation_level': {
|
|
58
|
+
code: 'isolation_level',
|
|
59
|
+
ru: 'удаление работает только на уровне `read committed`',
|
|
60
|
+
hint: 'удалять в транзакции уровня `read committed` — он стоит по умолчанию',
|
|
61
|
+
},
|
|
62
|
+
'codes.recreated_in_statement': {
|
|
63
|
+
code: 'recreated_in_statement',
|
|
64
|
+
ru: 'объект удалён и создан заново одним оператором',
|
|
65
|
+
hint: 'удалить и создать объект разными операторами',
|
|
66
|
+
},
|
|
67
|
+
'codes.delete_restricted': {
|
|
68
|
+
code: 'delete_restricted',
|
|
69
|
+
ru: 'удалить нельзя: на объект ссылаются строки с действием при удалении `restrict`',
|
|
70
|
+
hint: 'сначала удалить или перевести ссылающиеся строки — или сменить у их конца действие при удалении (`onDelete`)',
|
|
71
|
+
},
|
|
72
|
+
'codes.not_deleted': {
|
|
73
|
+
code: 'not_deleted',
|
|
74
|
+
ru: '`restore` работает только для удалённых объектов',
|
|
75
|
+
hint: 'сначала `delete()`, потом `restore()`',
|
|
76
|
+
},
|
|
77
|
+
'codes.exists': {
|
|
78
|
+
code: 'exists',
|
|
79
|
+
ru: 'объект с таким id уже существует',
|
|
80
|
+
hint: 'при повторе операции с ключом это ответ «уже записано»; иначе — `update`, `upsert` или другой ключ',
|
|
81
|
+
},
|
|
82
|
+
'codes.duplicate': {
|
|
83
|
+
code: 'duplicate',
|
|
84
|
+
ru: 'загрузчик: id повторяется во входных данных',
|
|
85
|
+
hint: 'убрать повторы id из входных данных: первые 100 — в `detail.rows`',
|
|
86
|
+
},
|
|
87
|
+
'codes.deleted': {
|
|
88
|
+
code: 'deleted',
|
|
89
|
+
ru: 'загрузчик: объект с этим id уже жил раньше и был удалён',
|
|
90
|
+
hint: "загрузить с `existing: 'update'` — объект оживёт новой версией — или дать ему другой id",
|
|
91
|
+
},
|
|
92
|
+
'codes.conflict': {
|
|
93
|
+
code: 'conflict',
|
|
94
|
+
ru: '`update` с `{ rev }`: строку уже изменили после того, как вы её прочли',
|
|
95
|
+
hint: 'перечитать строку и повторить правку с новым `rev`; для чисел — `inc` вместо записи итога',
|
|
96
|
+
},
|
|
97
|
+
'codes.rev_ambiguous': {
|
|
98
|
+
code: 'rev_ambiguous',
|
|
99
|
+
ru: '`{ rev }` указан у шага, который выбрал больше одного объекта',
|
|
100
|
+
hint: 'сузить шаг до одной строки (id или ключ) или убрать `{ rev }`',
|
|
101
|
+
},
|
|
102
|
+
'codes.tx_required': {
|
|
103
|
+
code: 'tx_required',
|
|
104
|
+
ru: '`forUpdate()`, `lock()`, `commit()` и `rollback()` работают только внутри `db.begin()`',
|
|
105
|
+
hint: 'вызывать внутри транзакции: `const tr = await db.begin()` … `await tr.commit()`',
|
|
106
|
+
},
|
|
107
|
+
'codes.lock_unsupported': {
|
|
108
|
+
code: 'lock_unsupported',
|
|
109
|
+
ru: '`forUpdate()` нельзя сочетать с агрегатом, `asOf`, `versions`, `withDeleted`, `deep` или глаголом',
|
|
110
|
+
hint: 'брать `forUpdate()` в простом чтении строк, а итоги, срезы и правки делать отдельными шагами',
|
|
111
|
+
},
|
|
112
|
+
'codes.commit_unknown': {
|
|
113
|
+
code: 'commit_unknown',
|
|
114
|
+
ru: 'связь оборвалась во время `COMMIT` — неизвестно, записалась ли транзакция',
|
|
115
|
+
hint: 'повторить ту же операцию с тем же ключом: `exists` — она уже записана, успех — в первый раз не записалась; без ключа исход не выяснить, поэтому операциям с деньгами нужен ключ',
|
|
116
|
+
},
|
|
117
|
+
'codes.tx_rolled_back': {
|
|
118
|
+
code: 'tx_rolled_back',
|
|
119
|
+
ru: 'PostgreSQL откатил транзакцию на `COMMIT`: ошибку перехватили внутри `tr.transaction(fn)`',
|
|
120
|
+
hint: 'не глушить ошибки внутри `tr.transaction(fn)`: устранить причину и повторить весь блок',
|
|
121
|
+
},
|
|
122
|
+
'codes.no_key': {
|
|
123
|
+
code: 'no_key',
|
|
124
|
+
ru: 'у класса нет ключа, а `upsert` и `idOf` без ключа не работают',
|
|
125
|
+
hint: 'дать классу ключ (`key` в модели) или работать по id: `update` вместо `upsert`',
|
|
126
|
+
},
|
|
127
|
+
'codes.no_path': {
|
|
128
|
+
code: 'no_path',
|
|
129
|
+
ru: 'между классами соседних шагов цепочки нет перехода',
|
|
130
|
+
hint: 'переходить по объявленным концам: между классами шагов нужна ссылка, роль конца — имя шага',
|
|
131
|
+
},
|
|
132
|
+
'codes.invalid_query': {
|
|
133
|
+
code: 'invalid_query',
|
|
134
|
+
ru: 'цепочка или вызов API построены неверно',
|
|
135
|
+
hint: 'исправить цепочку или вызов: текст ошибки называет неверный шаг или аргумент',
|
|
136
|
+
},
|
|
137
|
+
'codes.admin_required': {
|
|
138
|
+
code: 'admin_required',
|
|
139
|
+
ru: 'нужно подключение администратора: его требует загрузчик',
|
|
140
|
+
hint: 'запускать загрузчик на администраторском подключении (`allowBypassRls: true`) в отдельном процессе',
|
|
141
|
+
},
|
|
142
|
+
'codes.not_empty': {
|
|
143
|
+
code: 'not_empty',
|
|
144
|
+
ru: 'первая заливка возможна только в пустую схему',
|
|
145
|
+
hint: 'первую заливку делать в новую схему (`up({ fresh: true })`), в заполненную — загрузка без `initial`',
|
|
146
|
+
},
|
|
147
|
+
'codes.class_not_loadable': {
|
|
148
|
+
code: 'class_not_loadable',
|
|
149
|
+
ru: 'загрузчик не принимает строки классов (`Class`) — классы записывает `syncModels`',
|
|
150
|
+
hint: 'классы записывать `syncModels`, а загрузчику передавать только строки данных',
|
|
151
|
+
},
|
|
152
|
+
'codes.bypass_rls': {
|
|
153
|
+
code: 'bypass_rls',
|
|
154
|
+
ru: 'роль подключения обходит RLS: `connect()` без `allowBypassRls` или сервер MCP поверх такого подключения',
|
|
155
|
+
hint: 'подключать приложение отдельной ролью из группы `letopis_app`; администраторскому подключению — `allowBypassRls: true`; сервер MCP — только поверх подключения приложения',
|
|
156
|
+
},
|
|
157
|
+
'codes.user_session_required': {
|
|
158
|
+
code: 'user_session_required',
|
|
159
|
+
ru: 'серверу MCP нужна сессия пользователя, а не ключ сервиса или пустое подключение',
|
|
160
|
+
hint: 'создавать сервер MCP на подключении пользователя (`svc.as(…)` или `connect({ token })`), а не на корне сервиса',
|
|
161
|
+
},
|
|
162
|
+
'codes.confirmation_required': {
|
|
163
|
+
code: 'confirmation_required',
|
|
164
|
+
ru: 'MCP: удаление не подтвердил человек',
|
|
165
|
+
hint: 'подтвердить удаление в клиенте MCP; клиенту, который не умеет спрашивать человека, удаление недоступно',
|
|
166
|
+
},
|
|
167
|
+
// SQLSTATE XX000 с ключом каталога в DETAIL: база не держит условие, которое обязана держать сама (строка-замок)
|
|
168
|
+
'codes.internal': {
|
|
169
|
+
code: 'internal',
|
|
170
|
+
ru: 'внутренняя ошибка letopis: база не держит условие, которое обязана держать сама (например, нет строки-замка)',
|
|
171
|
+
hint: 'это ошибка letopis: сообщите о ней с текстом, кодом и версией пакета',
|
|
172
|
+
},
|
|
173
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Каталог сообщений letopis (план 2.0, этап 3, п. 3): ключ → код ошибки, шаблон текста и, если нужно, своя подсказка
|
|
3
|
+
* «что делать». Здесь разделы сливаются в MESSAGES (префикс ключа — имя раздела, ключи не повторяются), а render()
|
|
4
|
+
* подставляет параметры; ошибки и тексты строит errors.ts — lerr, lmsg, lwarn.
|
|
5
|
+
*
|
|
6
|
+
* Как перевести место на ключ каталога — инструкция исполнителям (образцы: записи разделов api, query, access, sql-core, sql-write,
|
|
7
|
+
* sql-time и места, которые их бросают).
|
|
8
|
+
*
|
|
9
|
+
* 1. Раздел — по файлу, где брошена ошибка:
|
|
10
|
+
* api — index, chain, typed, errors, internal; write — write, tx, load; query — sql, ops;
|
|
11
|
+
* time — watch, admin, cache, signals; access — auth, acl; model — model, registry, sync, cli, validate,
|
|
12
|
+
* jsonschema, uuid; boot — up, pglite; mcp — mcp, describe;
|
|
13
|
+
* sql-core — sql/10, 15, 20, 30, 40; sql-write — sql/60; sql-access — sql/50, 80;
|
|
14
|
+
* sql-time — sql/70, 90, 95, 99 и sql/migrate.
|
|
15
|
+
* codes.ts — запись на каждый код: заголовок и подсказка. Новый код — ещё ErrorCode (types.ts), для кода базы —
|
|
16
|
+
* LT_NAMES (errors.ts) и error_catalog(), и строка таблицы «Ошибки» README с якорем `<a id="код"></a>`.
|
|
17
|
+
* 2. Ключ — `<раздел>.<snake_case>`: что случилось, а не где (api.unknown_step, query.sort_field). Одно место — один
|
|
18
|
+
* ключ; одинаковый текст в разных разделах — разные ключи (api.validation_failed и sql-write.invalid_data).
|
|
19
|
+
* 3. Запись — { code, ru, hint? }, поля в этом порядке:
|
|
20
|
+
* code — код, с которым место бросало ошибку: код не меняется (коды сверяет check:semver);
|
|
21
|
+
* ru — прежний текст буква в букву, без префикса «letopis: <код>: »; подстановка ${x} → параметр {имя};
|
|
22
|
+
* hint — только если подсказка кода из codes.ts к месту не подходит (образцы: api.connect_schema, sql-time.last_owner).
|
|
23
|
+
* Параметр печатается как `${x}` в шаблонной строке, поэтому прежний текст не меняется; лучше скаляры (строка,
|
|
24
|
+
* число). Похожие имена — params.similar (массив из similar() в errors.ts): «— похожие: a, b» lerr допишет сам, в
|
|
25
|
+
* шаблон их не пишут.
|
|
26
|
+
* Текст с двумя формами (род, число) — две записи или параметр без грамматики, а не склейка слов в коде.
|
|
27
|
+
* 4. Вызов в TS:
|
|
28
|
+
* throw lerr('api.unknown_step', { name, similar: similar(name, names) });
|
|
29
|
+
* throw lerr('access.switch_not_root');
|
|
30
|
+
* throw lerr('<раздел>.<ключ>', { cls }, { detail: { id }, cause: e }); // detail и cause — как раньше
|
|
31
|
+
* Текст не для ошибки (сообщение нарушения, описание) — lmsg(ключ, параметры); предупреждение — lwarn(ключ,
|
|
32
|
+
* параметры, { once }). Подкласс и помощник вида fail(текст) переходят на ключ так же (образец — ValidationError в
|
|
33
|
+
* errors.ts). Старый конструктор new LetopisError(код, текст) из переведённого места уходит целиком.
|
|
34
|
+
* 5. Вызов в SQL — перегрузка lt_fail(p_key text, p_params jsonb default '{}', p_detail jsonb default null)
|
|
35
|
+
* (lib/sql/15-errors.sql): код ошибки — из записи, текст база собирает из шаблона ru (его видит и сырой SQL), в
|
|
36
|
+
* DETAIL — поля p_detail и ещё key, params; их fromDbError переносит в поля ошибки (key, params):
|
|
37
|
+
* perform @SCHEMA@.lt_fail('sql-core.no_membership');
|
|
38
|
+
* perform @SCHEMA@.lt_fail('sql-write.invalid_data', jsonb_build_object('cls', NEW.class), jsonb_build_object('issues', bad));
|
|
39
|
+
* Параметры — jsonb_build_object со скалярами (список — array_to_string); без параметров — один аргумент, а не null
|
|
40
|
+
* (литерал без типа выбрал бы прежнюю lt_fail(имя, текст)). detail ошибки — прежний: без p_detail — {}, а место, где
|
|
41
|
+
* был raise без detail, — p_detail => 'null' (detail нет). Свой SQLSTATE не из каталога — lt_raise(p_code, p_key,
|
|
42
|
+
* p_params, p_detail). В функциях, которые исполняет приложение (inc, links_patch: lt_fail ему не выдан), — raise
|
|
43
|
+
* как прежде (errcode — SQLSTATE кода, message — префикс «letopis: <код>: » и текст), только текст — lt_text(ключ,
|
|
44
|
+
* параметры) (она выдана приложению), а detail — jsonb_build_object('key', ключ, 'params', параметры)::text; без
|
|
45
|
+
* своих полей — ещё 'nodetail', true (detail нет, как у raise без detail).
|
|
46
|
+
* Разделы sql-* установщик кладёт в базу при каждом up() (@MESSAGES@): новой ревизии и миграции не нужно.
|
|
47
|
+
* 6. Готовый файл — убрать из PENDING_LITERALS в lib/scripts/check-docs.mjs (проверка «в LetopisError, fail и lt_fail не
|
|
48
|
+
* осталось литеральных текстов»). Затем cd lib && npm run typecheck, npm run check:docs, npm run api:contract и тесты
|
|
49
|
+
* модуля по одному файлу: тест, который сверял русский текст, остаётся зелёным без правки.
|
|
50
|
+
* 7. Особые места. Разделы каталога контракт API читает регэкспами: код — из поля code: в кавычках, текст ошибки — из
|
|
51
|
+
* строки с префиксом letopis и двоеточием; в комментариях их в таком виде не пишут. up.ts и pglite.ts бросают обычный Error('letopis.up: ' + lmsg(…)) — подходящего кода нет. Сообщения нарушений
|
|
52
|
+
* (issues[].message: validate.ts, 30-validate.sql) — lmsg и lt_text с кодом invalid_data. Описания инструментов MCP и
|
|
53
|
+
* instructions — строки в mcp.ts рядом со схемами, тексты ответов — раздел mcp.
|
|
54
|
+
*/
|
|
55
|
+
import { access } from './access.js';
|
|
56
|
+
import { api } from './api.js';
|
|
57
|
+
import { boot } from './boot.js';
|
|
58
|
+
import { codes } from './codes.js';
|
|
59
|
+
import { mcp } from './mcp.js';
|
|
60
|
+
import { model } from './model.js';
|
|
61
|
+
import { query } from './query.js';
|
|
62
|
+
import { sqlAccess } from './sql-access.js';
|
|
63
|
+
import { sqlCore } from './sql-core.js';
|
|
64
|
+
import { sqlTime } from './sql-time.js';
|
|
65
|
+
import { sqlWrite } from './sql-write.js';
|
|
66
|
+
import { time } from './time.js';
|
|
67
|
+
import { write } from './write.js';
|
|
68
|
+
import type { ErrorCode, Msg, Params } from './types.js';
|
|
69
|
+
export type { ErrorCode, Msg, Params, Section } from './types.js';
|
|
70
|
+
/** Разделы, которые установщик кладёт в базу (message_catalog() в lib/sql/15-errors.sql). */
|
|
71
|
+
export declare const SQL_SECTIONS: readonly ["sql-core", "sql-write", "sql-access", "sql-time"];
|
|
72
|
+
type All = typeof codes & typeof api & typeof write & typeof query & typeof time & typeof access & typeof model & typeof boot & typeof mcp & typeof sqlCore & typeof sqlWrite & typeof sqlAccess & typeof sqlTime;
|
|
73
|
+
/** Ключ каталога: опечатку в lerr('…') ловит проверка типов. */
|
|
74
|
+
export type MsgKey = keyof All;
|
|
75
|
+
/** Все записи каталога по ключу. */
|
|
76
|
+
export declare const MESSAGES: Readonly<Record<MsgKey, Msg>>;
|
|
77
|
+
/** Запись по ключу из любого источника (DETAIL базы, вызов из JS); нет такой — undefined. */
|
|
78
|
+
export declare function msgOf(key: string): Msg | undefined;
|
|
79
|
+
/** Подпись кода: заголовок и подсказка «что делать» (codes.ts). */
|
|
80
|
+
export declare function codeOf(code: string): (Msg & {
|
|
81
|
+
hint: string;
|
|
82
|
+
}) | undefined;
|
|
83
|
+
/** Суффикс похожих имён (params.similar) — его дописывает lerr. */
|
|
84
|
+
export declare const SIMILAR = " \u2014 \u043F\u043E\u0445\u043E\u0436\u0438\u0435: {similar}";
|
|
85
|
+
/**
|
|
86
|
+
* Шаблон с параметрами: `{имя}` → значение, напечатанное как в шаблонной строке JS (`${x}`), поэтому перенос текста
|
|
87
|
+
* `…${x}…` в шаблон `…{x}…` его не меняет. `{имя}` без такого параметра остаётся как есть; так же — lt_text() в базе
|
|
88
|
+
* (там null — пусто, как у format('%s')).
|
|
89
|
+
*/
|
|
90
|
+
export declare function render(template: string, params?: Params): string;
|
|
91
|
+
/** Разделы sql-* для базы: ключ → имя ошибки и русский шаблон (установщик подставляет в @MESSAGES@). */
|
|
92
|
+
export declare function sqlCatalog(): Record<string, {
|
|
93
|
+
name: ErrorCode;
|
|
94
|
+
ru: string;
|
|
95
|
+
}>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Каталог сообщений letopis (план 2.0, этап 3, п. 3): ключ → код ошибки, шаблон текста и, если нужно, своя подсказка
|
|
3
|
+
* «что делать». Здесь разделы сливаются в MESSAGES (префикс ключа — имя раздела, ключи не повторяются), а render()
|
|
4
|
+
* подставляет параметры; ошибки и тексты строит errors.ts — lerr, lmsg, lwarn.
|
|
5
|
+
*
|
|
6
|
+
* Как перевести место на ключ каталога — инструкция исполнителям (образцы: записи разделов api, query, access, sql-core, sql-write,
|
|
7
|
+
* sql-time и места, которые их бросают).
|
|
8
|
+
*
|
|
9
|
+
* 1. Раздел — по файлу, где брошена ошибка:
|
|
10
|
+
* api — index, chain, typed, errors, internal; write — write, tx, load; query — sql, ops;
|
|
11
|
+
* time — watch, admin, cache, signals; access — auth, acl; model — model, registry, sync, cli, validate,
|
|
12
|
+
* jsonschema, uuid; boot — up, pglite; mcp — mcp, describe;
|
|
13
|
+
* sql-core — sql/10, 15, 20, 30, 40; sql-write — sql/60; sql-access — sql/50, 80;
|
|
14
|
+
* sql-time — sql/70, 90, 95, 99 и sql/migrate.
|
|
15
|
+
* codes.ts — запись на каждый код: заголовок и подсказка. Новый код — ещё ErrorCode (types.ts), для кода базы —
|
|
16
|
+
* LT_NAMES (errors.ts) и error_catalog(), и строка таблицы «Ошибки» README с якорем `<a id="код"></a>`.
|
|
17
|
+
* 2. Ключ — `<раздел>.<snake_case>`: что случилось, а не где (api.unknown_step, query.sort_field). Одно место — один
|
|
18
|
+
* ключ; одинаковый текст в разных разделах — разные ключи (api.validation_failed и sql-write.invalid_data).
|
|
19
|
+
* 3. Запись — { code, ru, hint? }, поля в этом порядке:
|
|
20
|
+
* code — код, с которым место бросало ошибку: код не меняется (коды сверяет check:semver);
|
|
21
|
+
* ru — прежний текст буква в букву, без префикса «letopis: <код>: »; подстановка ${x} → параметр {имя};
|
|
22
|
+
* hint — только если подсказка кода из codes.ts к месту не подходит (образцы: api.connect_schema, sql-time.last_owner).
|
|
23
|
+
* Параметр печатается как `${x}` в шаблонной строке, поэтому прежний текст не меняется; лучше скаляры (строка,
|
|
24
|
+
* число). Похожие имена — params.similar (массив из similar() в errors.ts): «— похожие: a, b» lerr допишет сам, в
|
|
25
|
+
* шаблон их не пишут.
|
|
26
|
+
* Текст с двумя формами (род, число) — две записи или параметр без грамматики, а не склейка слов в коде.
|
|
27
|
+
* 4. Вызов в TS:
|
|
28
|
+
* throw lerr('api.unknown_step', { name, similar: similar(name, names) });
|
|
29
|
+
* throw lerr('access.switch_not_root');
|
|
30
|
+
* throw lerr('<раздел>.<ключ>', { cls }, { detail: { id }, cause: e }); // detail и cause — как раньше
|
|
31
|
+
* Текст не для ошибки (сообщение нарушения, описание) — lmsg(ключ, параметры); предупреждение — lwarn(ключ,
|
|
32
|
+
* параметры, { once }). Подкласс и помощник вида fail(текст) переходят на ключ так же (образец — ValidationError в
|
|
33
|
+
* errors.ts). Старый конструктор new LetopisError(код, текст) из переведённого места уходит целиком.
|
|
34
|
+
* 5. Вызов в SQL — перегрузка lt_fail(p_key text, p_params jsonb default '{}', p_detail jsonb default null)
|
|
35
|
+
* (lib/sql/15-errors.sql): код ошибки — из записи, текст база собирает из шаблона ru (его видит и сырой SQL), в
|
|
36
|
+
* DETAIL — поля p_detail и ещё key, params; их fromDbError переносит в поля ошибки (key, params):
|
|
37
|
+
* perform @SCHEMA@.lt_fail('sql-core.no_membership');
|
|
38
|
+
* perform @SCHEMA@.lt_fail('sql-write.invalid_data', jsonb_build_object('cls', NEW.class), jsonb_build_object('issues', bad));
|
|
39
|
+
* Параметры — jsonb_build_object со скалярами (список — array_to_string); без параметров — один аргумент, а не null
|
|
40
|
+
* (литерал без типа выбрал бы прежнюю lt_fail(имя, текст)). detail ошибки — прежний: без p_detail — {}, а место, где
|
|
41
|
+
* был raise без detail, — p_detail => 'null' (detail нет). Свой SQLSTATE не из каталога — lt_raise(p_code, p_key,
|
|
42
|
+
* p_params, p_detail). В функциях, которые исполняет приложение (inc, links_patch: lt_fail ему не выдан), — raise
|
|
43
|
+
* как прежде (errcode — SQLSTATE кода, message — префикс «letopis: <код>: » и текст), только текст — lt_text(ключ,
|
|
44
|
+
* параметры) (она выдана приложению), а detail — jsonb_build_object('key', ключ, 'params', параметры)::text; без
|
|
45
|
+
* своих полей — ещё 'nodetail', true (detail нет, как у raise без detail).
|
|
46
|
+
* Разделы sql-* установщик кладёт в базу при каждом up() (@MESSAGES@): новой ревизии и миграции не нужно.
|
|
47
|
+
* 6. Готовый файл — убрать из PENDING_LITERALS в lib/scripts/check-docs.mjs (проверка «в LetopisError, fail и lt_fail не
|
|
48
|
+
* осталось литеральных текстов»). Затем cd lib && npm run typecheck, npm run check:docs, npm run api:contract и тесты
|
|
49
|
+
* модуля по одному файлу: тест, который сверял русский текст, остаётся зелёным без правки.
|
|
50
|
+
* 7. Особые места. Разделы каталога контракт API читает регэкспами: код — из поля code: в кавычках, текст ошибки — из
|
|
51
|
+
* строки с префиксом letopis и двоеточием; в комментариях их в таком виде не пишут. up.ts и pglite.ts бросают обычный Error('letopis.up: ' + lmsg(…)) — подходящего кода нет. Сообщения нарушений
|
|
52
|
+
* (issues[].message: validate.ts, 30-validate.sql) — lmsg и lt_text с кодом invalid_data. Описания инструментов MCP и
|
|
53
|
+
* instructions — строки в mcp.ts рядом со схемами, тексты ответов — раздел mcp.
|
|
54
|
+
*/
|
|
55
|
+
import { access } from './access.js';
|
|
56
|
+
import { api } from './api.js';
|
|
57
|
+
import { boot } from './boot.js';
|
|
58
|
+
import { codes } from './codes.js';
|
|
59
|
+
import { mcp } from './mcp.js';
|
|
60
|
+
import { model } from './model.js';
|
|
61
|
+
import { query } from './query.js';
|
|
62
|
+
import { sqlAccess } from './sql-access.js';
|
|
63
|
+
import { sqlCore } from './sql-core.js';
|
|
64
|
+
import { sqlTime } from './sql-time.js';
|
|
65
|
+
import { sqlWrite } from './sql-write.js';
|
|
66
|
+
import { time } from './time.js';
|
|
67
|
+
import { write } from './write.js';
|
|
68
|
+
/** Разделы каталога: имя — префикс ключа и имя файла. */
|
|
69
|
+
const SECTIONS = {
|
|
70
|
+
codes, api, write, query, time, access, model, boot, mcp,
|
|
71
|
+
'sql-core': sqlCore, 'sql-write': sqlWrite, 'sql-access': sqlAccess, 'sql-time': sqlTime,
|
|
72
|
+
};
|
|
73
|
+
/** Разделы, которые установщик кладёт в базу (message_catalog() в lib/sql/15-errors.sql). */
|
|
74
|
+
export const SQL_SECTIONS = ['sql-core', 'sql-write', 'sql-access', 'sql-time'];
|
|
75
|
+
function merge(sections) {
|
|
76
|
+
const out = {};
|
|
77
|
+
for (const [name, section] of Object.entries(sections)) {
|
|
78
|
+
for (const [key, m] of Object.entries(section)) {
|
|
79
|
+
if (!key.startsWith(`${name}.`))
|
|
80
|
+
throw new Error(`letopis: ключ каталога ${key} — не из раздела ${name}`);
|
|
81
|
+
if (Object.hasOwn(out, key))
|
|
82
|
+
throw new Error(`letopis: ключ каталога ${key} повторяется`);
|
|
83
|
+
out[key] = m;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
/** Все записи каталога по ключу. */
|
|
89
|
+
export const MESSAGES = merge(SECTIONS);
|
|
90
|
+
/** Запись по ключу из любого источника (DETAIL базы, вызов из JS); нет такой — undefined. */
|
|
91
|
+
export function msgOf(key) {
|
|
92
|
+
return Object.hasOwn(MESSAGES, key) ? MESSAGES[key] : undefined;
|
|
93
|
+
}
|
|
94
|
+
/** Подпись кода: заголовок и подсказка «что делать» (codes.ts). */
|
|
95
|
+
export function codeOf(code) {
|
|
96
|
+
return msgOf(`codes.${code}`);
|
|
97
|
+
}
|
|
98
|
+
/** Суффикс похожих имён (params.similar) — его дописывает lerr. */
|
|
99
|
+
export const SIMILAR = ' — похожие: {similar}';
|
|
100
|
+
/**
|
|
101
|
+
* Шаблон с параметрами: `{имя}` → значение, напечатанное как в шаблонной строке JS (`${x}`), поэтому перенос текста
|
|
102
|
+
* `…${x}…` в шаблон `…{x}…` его не меняет. `{имя}` без такого параметра остаётся как есть; так же — lt_text() в базе
|
|
103
|
+
* (там null — пусто, как у format('%s')).
|
|
104
|
+
*/
|
|
105
|
+
export function render(template, params) {
|
|
106
|
+
if (!params)
|
|
107
|
+
return template;
|
|
108
|
+
return template.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (m, k) => (Object.hasOwn(params, k) ? String(params[k]) : m));
|
|
109
|
+
}
|
|
110
|
+
/** Разделы sql-* для базы: ключ → имя ошибки и русский шаблон (установщик подставляет в @MESSAGES@). */
|
|
111
|
+
export function sqlCatalog() {
|
|
112
|
+
const out = {};
|
|
113
|
+
for (const s of SQL_SECTIONS) {
|
|
114
|
+
for (const [key, m] of Object.entries(SECTIONS[s]))
|
|
115
|
+
out[key] = { name: m.code, ru: m.ru };
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|