{"id":483290,"date":"2026-06-11T08:53:22","date_gmt":"2026-06-11T08:53:22","guid":{"rendered":"https:\/\/savepearlharbor.com\/?p=483290"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=483290","title":{"rendered":"Schema-driven ORM \u0434\u043b\u044f TypeScript: @cleverbrush\/knex-schema \u0438 @cleverbrush\/orm"},"content":{"rendered":"<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u0421\u0442\u0430\u0442\u044c\u044f \u043e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u0440\u0435\u0432\u0440\u0430\u0442\u0438\u0442\u044c TypeScript-\u0441\u0445\u0435\u043c\u0443 \u0432 \u0435\u0434\u0438\u043d\u044b\u0439 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0438\u0441\u0442\u0438\u043d\u044b \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0431\u0430\u0437\u043e\u0439 \u0434\u0430\u043d\u043d\u044b\u0445: \u0438\u0437 \u043e\u0434\u043d\u043e\u0433\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u0438\u043f\u044b \u0441\u0442\u0440\u043e\u043a, \u0438\u043c\u0435\u043d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446 \u0438 \u043a\u043e\u043b\u043e\u043d\u043e\u043a, \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u044b, \u0441\u0432\u044f\u0437\u0438, \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0438 unit-of-work \u043f\u043e\u0432\u0435\u0440\u0445 Knex.<\/p>\n<p>\u0412\u0441\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u044b \u043d\u0438\u0436\u0435 \u0432\u0437\u044f\u0442\u044b \u0438\u0437 <a href=\"https:\/\/xpenser.cleverbrush.com\" rel=\"noopener noreferrer nofollow\">xpenser<\/a> \u2014 open-source \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u0443\u0447\u0451\u0442\u0430 \u043b\u0438\u0447\u043d\u044b\u0445 \u0434\u043e\u0445\u043e\u0434\u043e\u0432 \u0438 \u0440\u0430\u0441\u0445\u043e\u0434\u043e\u0432. \u042d\u0442\u043e \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u043f\u043e\u043b\u0435\u0437\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u044f \u0441\u0430\u043c \u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0441\u044c, \u0438 \u0440\u0435\u0444\u0435\u0440\u0435\u043d\u0441\u043d\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f Cleverbrush Framework: \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u044b, \u0441\u0435\u0440\u0432\u0435\u0440, \u043a\u043b\u0438\u0435\u043d\u0442, \u0444\u043e\u0440\u043c\u044b, auth, observability, PostgreSQL, Telegram bot \u0438 MCP endpoint \u0436\u0438\u0432\u0443\u0442 \u0432 \u043e\u0434\u043d\u043e\u043c \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438. \u041a\u043e\u0434 \u043e\u0442\u043a\u0440\u044b\u0442: <a href=\"https:\/\/github.com\/cleverbrush\/xpenser\" rel=\"noopener noreferrer nofollow\">github.com\/cleverbrush\/xpenser<\/a>.<\/p>\n<p>\u0414\u0438\u0441\u043a\u043b\u0435\u0439\u043c\u0435\u0440: \u0432\u0441\u0435 \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u043c\u044b\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u043d\u043e\u0441\u044f\u0442 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0439 \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440. \u041d\u0435\u0441\u043c\u043e\u0442\u0440\u044f \u043d\u0430 \u044d\u0442\u043e, \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435 \u0442\u0435\u0441\u0442\u0430\u043c\u0438 \u0443 \u043d\u0438\u0445 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0445\u043e\u0440\u043e\u0448\u0435\u0435. \u0422\u0430\u043a\u0436\u0435 \u0432 \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0432 \u044d\u0442\u0438\u0445 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430\u0445 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043b\u0438\u0448\u044c Postgres \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0431\u0430\u0437 \u0434\u0430\u043d\u043d\u044b\u0445.<\/p>\n<h3>\u041f\u0440\u0435\u0434\u044b\u0441\u0442\u043e\u0440\u0438\u044f<\/h3>\n<p>\u0412 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0445 \u0441\u0442\u0430\u0442\u044c\u044f\u0445 \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u044b\u0432\u0430\u043b \u043e <a href=\"https:\/\/habr.com\/ru\/articles\/1023038\/\" rel=\"noopener noreferrer nofollow\"><code>@cleverbrush\/schema<\/code><\/a>, \u043e \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c HTTP API \u0447\u0435\u0440\u0435\u0437 <a href=\"https:\/\/habr.com\/ru\/articles\/1030342\/\" rel=\"noopener noreferrer nofollow\"><code>@cleverbrush\/server<\/code> \u0438 <code>@cleverbrush\/client<\/code><\/a>, \u0430 \u0437\u0430\u0442\u0435\u043c \u043e <a href=\"https:\/\/habr.com\/ru\/articles\/1040714\/\" rel=\"noopener noreferrer nofollow\"><code>@cleverbrush\/log<\/code> \u0438 <code>@cleverbrush\/otel<\/code><\/a>.<\/p>\n<p>\u041e\u0431\u0449\u0430\u044f \u0438\u0434\u0435\u044f \u0443 \u0432\u0441\u0435\u0445 \u044d\u0442\u0438\u0445 \u043f\u0430\u043a\u0435\u0442\u043e\u0432 \u043e\u0434\u043d\u0430: \u0441\u0445\u0435\u043c\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u043d\u0435 \u043f\u0440\u043e\u0441\u0442\u043e \u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440\u043e\u043c, \u0430 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u043c \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438. \u0415\u0441\u043b\u0438 \u0441\u0445\u0435\u043c\u0430 \u0443\u0436\u0435 \u0437\u043d\u0430\u0435\u0442, \u0447\u0442\u043e \u0443 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0435\u0441\u0442\u044c \u043f\u043e\u043b\u0435 <code>userId<\/code>, \u0447\u0442\u043e \u044d\u0442\u043e \u0447\u0438\u0441\u043b\u043e, \u0447\u0442\u043e \u043e\u043d\u043e \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u0438 \u0447\u0442\u043e \u0434\u043b\u044f \u043d\u0435\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c PropertyDescriptor \u0447\u0435\u0440\u0435\u0437 <code>t =&gt; t.userId<\/code>, \u0442\u043e \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u0435\u0442 \u0435\u0441\u0442\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0432\u043e\u043f\u0440\u043e\u0441: \u043f\u043e\u0447\u0435\u043c\u0443 \u0431\u044b \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e \u0436\u0435 \u0437\u043d\u0430\u043d\u0438\u0435 \u0434\u043b\u044f SQL?<\/p>\n<p>\u041d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0435 \u043e\u0442\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u0442\u0435\u0445, \u043a\u0442\u043e \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u043b \u0441 Knex. <a href=\"https:\/\/knexjs.org\" rel=\"noopener noreferrer nofollow\">Knex<\/a> \u2014 \u044d\u0442\u043e query builder \u0434\u043b\u044f Node.js: \u043e\u043d \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u0438\u0441\u0430\u0442\u044c SQL-\u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0446\u0435\u043f\u043e\u0447\u043a\u0430\u043c\u0438 \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u0432\u043c\u0435\u0441\u0442\u043e \u0440\u0443\u0447\u043d\u043e\u0439 \u0441\u0431\u043e\u0440\u043a\u0438 \u0441\u0442\u0440\u043e\u043a, \u0443\u043c\u0435\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0441 PostgreSQL, MySQL, SQLite \u0438 \u0434\u0440\u0443\u0433\u0438\u043c\u0438 SQL-\u0431\u0430\u0437\u0430\u043c\u0438, \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438, connection pool, migrations \u0438 schema builder.<\/p>\n<p>\u041b\u044e\u0434\u0438 \u0447\u0430\u0441\u0442\u043e \u0432\u044b\u0431\u0438\u0440\u0430\u044e\u0442 Knex, \u043a\u043e\u0433\u0434\u0430 \u0445\u043e\u0442\u044f\u0442 \u043e\u0441\u0442\u0430\u0432\u0430\u0442\u044c\u0441\u044f \u0431\u043b\u0438\u0437\u043a\u043e \u043a SQL, \u043d\u043e \u043d\u0435 \u0445\u043e\u0442\u044f\u0442 \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0430\u0437 \u0440\u0443\u043a\u0430\u043c\u0438 \u0441\u043e\u0431\u0438\u0440\u0430\u0442\u044c <code>SELECT ... WHERE ...<\/code> \u0438 \u0441\u043b\u0435\u0434\u0438\u0442\u044c \u0437\u0430 \u043f\u043b\u0435\u0439\u0441\u0445\u043e\u043b\u0434\u0435\u0440\u0430\u043c\u0438. \u042d\u0442\u043e \u0445\u043e\u0440\u043e\u0448\u0438\u0439 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043e\u0447\u043d\u044b\u0439 \u0441\u043b\u043e\u0439 \u043c\u0435\u0436\u0434\u0443 raw SQL \u0438 \u0442\u044f\u0436\u0451\u043b\u043e\u0439 ORM: \u043c\u0435\u043d\u044c\u0448\u0435 \u043c\u0430\u0433\u0438\u0438, \u043f\u0440\u043e\u0449\u0435 \u043f\u0440\u0435\u0434\u0441\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u0442\u043e\u0433\u043e\u0432\u044b\u0439 \u0437\u0430\u043f\u0440\u043e\u0441, \u043b\u0435\u0433\u043a\u043e \u0443\u043f\u0430\u0441\u0442\u044c \u043d\u0430 \u043e\u0431\u044b\u0447\u043d\u044b\u0439 SQL \u0442\u0430\u043c, \u0433\u0434\u0435 query builder \u043c\u0435\u0448\u0430\u0435\u0442.<\/p>\n<p>\u041d\u043e \u0443 Knex \u0435\u0441\u0442\u044c \u0435\u0441\u0442\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u043f\u0440\u0435\u0434\u0435\u043b. TypeScript \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u043c\u043e\u0447\u044c \u0441 \u0442\u0438\u043f\u043e\u043c \u0441\u0442\u0440\u043e\u043a\u0438 \u0447\u0435\u0440\u0435\u0437 <code>knex&lt;User&gt;('users')<\/code>, \u043d\u043e \u0438\u043c\u0435\u043d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446 \u0438 \u043a\u043e\u043b\u043e\u043d\u043e\u043a \u0432\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u0447\u0430\u0441\u0442\u043e \u043e\u0441\u0442\u0430\u044e\u0442\u0441\u044f \u0441\u0442\u0440\u043e\u043a\u0430\u043c\u0438: <code>'users'<\/code>, <code>'user_id'<\/code>, <code>'created_at'<\/code>. \u0415\u0441\u043b\u0438 \u0432 \u043a\u043e\u0434\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u0436\u0438\u0432\u0451\u0442 \u0432 camelCase, \u0431\u0430\u0437\u0430 \u0432 snake_case, \u0430 \u0440\u044f\u0434\u043e\u043c \u0435\u0449\u0451 \u0435\u0441\u0442\u044c API-\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u044b \u0438 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f, \u043f\u043e\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0440\u0438\u0441\u043a \u0440\u0430\u0441\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438. \u0418\u043c\u0435\u043d\u043d\u043e \u044d\u0442\u043e\u0442 \u0437\u0430\u0437\u043e\u0440 \u044f \u0438 \u0445\u043e\u0442\u0435\u043b \u0437\u0430\u043a\u0440\u044b\u0442\u044c schema-driven \u0441\u043b\u043e\u0435\u043c \u043f\u043e\u0432\u0435\u0440\u0445 Knex.<\/p>\n<p>\u0422\u0430\u043a \u043f\u043e\u044f\u0432\u0438\u043b\u0438\u0441\u044c \u0434\u0432\u0430 \u043f\u0430\u043a\u0435\u0442\u0430:<\/p>\n<ul>\n<li>\n<p><code>@cleverbrush\/knex-schema<\/code> \u2014 schema-aware \u0441\u043b\u043e\u0439 \u043f\u043e\u0432\u0435\u0440\u0445 Knex: \u0438\u043c\u0435\u043d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446 \u0438 \u043a\u043e\u043b\u043e\u043d\u043e\u043a, DDL-\u043c\u0435\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u0435, \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 query builder, projections, scopes, eager loading.<\/p>\n<\/li>\n<li>\n<p><code>@cleverbrush\/orm<\/code> \u2014 \u0431\u043e\u043b\u0435\u0435 \u0432\u044b\u0441\u043e\u043a\u0438\u0439 \u0443\u0440\u043e\u0432\u0435\u043d\u044c: <code>defineEntity()<\/code>, <code>DbContext<\/code>, <code>DbSet<\/code>, relations, \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438, save graph \u0438 tracking context.<\/p>\n<\/li>\n<\/ul>\n<p>Knex \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u043d\u0438\u043a\u0443\u0434\u0430 \u043d\u0435 \u0438\u0441\u0447\u0435\u0437\u0430\u0435\u0442. \u042d\u0442\u043e \u043d\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c SQL \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e. \u0421\u043a\u043e\u0440\u0435\u0435 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442: Knex \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u043d\u0438\u0436\u043d\u0438\u043c \u0443\u0440\u043e\u0432\u043d\u0435\u043c \u0438 escape hatch, \u0430 \u0441\u0445\u0435\u043c\u0430 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043a \u043d\u0435\u043c\u0443 \u0442\u0438\u043f\u044b \u0438 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0451\u043d\u043d\u043e\u0441\u0442\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u043e\u0431\u044b\u0447\u043d\u043e \u043d\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442 \u0432 \u0431\u043e\u043b\u044c\u0448\u043e\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438.<\/p>\n<h3>\u0421 \u0447\u0435\u0433\u043e \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f \u0442\u0430\u0431\u043b\u0438\u0446\u0430<\/h3>\n<p>\u0412 xpenser \u0442\u0430\u0431\u043b\u0438\u0446\u044b \u043e\u043f\u0438\u0441\u0430\u043d\u044b \u0432 <code>apps\/api\/src\/db\/schemas.ts<\/code>. \u0418\u043c\u043f\u043e\u0440\u0442 \u0438\u0434\u0451\u0442 \u0438\u0437 <code>@cleverbrush\/orm<\/code>, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e ORM \u0440\u0435\u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u0442 \u0432\u0441\u0435 schema builders \u0438\u0437 <code>@cleverbrush\/knex-schema<\/code> \u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043a \u043d\u0438\u043c \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u0435\u0442\u043e\u0434\u044b \u0434\u043b\u044f \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0441\u0432\u044f\u0437\u0435\u0439, DDL-\u043c\u0435\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u0445, \u043f\u0440\u043e\u0435\u043a\u0446\u0438\u0439, \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0441\u0432\u043e\u0439\u0441\u0442\u0432 \u0438 \u0442.\u0434.:<\/p>\n<pre><code class=\"typescript\">import {    boolean,    type DbContext,    date,    defineEntity,    number,    object,    string} from '@cleverbrush\/orm';<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:87px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041d\u0430\u0447\u043d\u0451\u043c \u0441 \u043f\u0440\u043e\u0441\u0442\u043e\u0439, \u043d\u043e \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0445\u0435\u043c\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f:<\/p>\n<pre><code class=\"typescript\">export const UserDbSchema = object({    id: number().primaryKey(),    email: string(),    passwordHash: string().optional().hasColumnName('password_hash'),    emailVerified: boolean().hasColumnName('email_verified').defaultTo(false),    role: string(),    authProvider: string().hasColumnName('auth_provider'),    defaultCurrency: string().hasColumnName('default_currency'),    countryCode: string().hasColumnName('country_code').defaultTo('US'),    timezone: string().defaultTo('UTC'),    createdAt: date().hasColumnName('created_at').defaultTo('now'),    updatedAt: date().hasColumnName('updated_at').defaultTo('now')})    .hasTableName('users')    .projection(        'public',        'id',        'email',        'emailVerified',        'role',        'authProvider',        'defaultCurrency',        'countryCode',        'timezone',        'createdAt',        'updatedAt'    )    .projection(        'auth',        'id',        'email',        'passwordHash',        'emailVerified',        'role',        'authProvider',        'defaultCurrency',        'countryCode',        'timezone'    );<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u043e\u0431\u044b\u0447\u043d\u0430\u044f schema-\u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f DB-\u043c\u0435\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u043c\u0438:<\/p>\n<ul>\n<li>\n<p><code>.hasTableName('users')<\/code> \u0437\u0430\u0434\u0430\u0451\u0442 \u0438\u043c\u044f \u0442\u0430\u0431\u043b\u0438\u0446\u044b.<\/p>\n<\/li>\n<li>\n<p><code>.hasColumnName('password_hash')<\/code> \u0441\u0432\u044f\u0437\u044b\u0432\u0430\u0435\u0442 camelCase-\u043f\u043e\u043b\u0435 \u0432 TypeScript \u0441\u043e snake_case-\u043a\u043e\u043b\u043e\u043d\u043a\u043e\u0439 \u0432 SQL.<\/p>\n<\/li>\n<li>\n<p><code>.primaryKey()<\/code> \u043f\u043e\u043c\u0435\u0447\u0430\u0435\u0442 \u043f\u0435\u0440\u0432\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447.<\/p>\n<\/li>\n<li>\n<p><code>.defaultTo(false)<\/code> \u0438 <code>.defaultTo('now')<\/code> \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u044e\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 DDL.<\/p>\n<\/li>\n<li>\n<p><code>.projection('auth', ...)<\/code> \u0437\u0430\u0434\u0430\u0451\u0442 \u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043d\u0430\u0431\u043e\u0440 \u043a\u043e\u043b\u043e\u043d\u043e\u043a \u0434\u043b\u044f \u0432\u044b\u0431\u043e\u0440\u043a\u0438.<\/p>\n<\/li>\n<\/ul>\n<p>\u0412 \u043e\u0431\u044b\u0447\u043d\u043e\u043c Knex \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c generic-\u0442\u0438\u043f\u044b \u0432 <code>knex&lt;User&gt;('users')<\/code>, \u043d\u043e \u0438\u043c\u0435\u043d\u0430 \u043a\u043e\u043b\u043e\u043d\u043e\u043a \u0432\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u043e\u0441\u0442\u0430\u044e\u0442\u0441\u044f \u0441\u0442\u0440\u043e\u043a\u0430\u043c\u0438. \u0417\u0434\u0435\u0441\u044c TypeScript-\u0441\u0432\u043e\u0439\u0441\u0442\u0432\u043e \u0438 SQL-\u043a\u043e\u043b\u043e\u043d\u043a\u0430 \u0441\u0432\u044f\u0437\u0430\u043d\u044b \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u0432 \u0441\u0445\u0435\u043c\u0435, \u0430 \u0434\u0430\u043b\u044c\u0448\u0435 query builder \u0441\u0430\u043c \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0438\u0442 <code>passwordHash<\/code> \u0432 <code>password_hash<\/code>.<\/p>\n<h3>Projections: \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 SELECT<\/h3>\n<p>\u0412 xpenser projections \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0442\u0430\u043c, \u0433\u0434\u0435 \u043d\u0435\u043b\u044c\u0437\u044f \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e \u0434\u043e\u0441\u0442\u0430\u0442\u044c \u043b\u0438\u0448\u043d\u0435\u0435 \u043f\u043e\u043b\u0435. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u0440\u0438 \u043b\u043e\u0433\u0438\u043d\u0435 \u043d\u0443\u0436\u0435\u043d <code>passwordHash<\/code>, \u043d\u043e \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u043e\u0442\u0432\u0435\u0442\u044b API \u043d\u0435 \u0434\u043e\u043b\u0436\u043d\u044b \u0435\u0433\u043e \u0432\u0438\u0434\u0435\u0442\u044c:<\/p>\n<pre><code class=\"typescript\">const user = await db.users    .projected('auth')    .where(candidate =&gt; candidate.email, email)    .first();<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>projected('auth')<\/code> \u0434\u0435\u043b\u0430\u0435\u0442 \u0434\u0432\u0435 \u0432\u0435\u0449\u0438 \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e:<\/p>\n<ul>\n<li>\n<p>\u0432 SQL \u0443\u0445\u043e\u0434\u0438\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0430\u0431\u043e\u0440 \u043a\u043e\u043b\u043e\u043d\u043e\u043a \u0438\u0437 <code>.projection('auth', ...)<\/code>;<\/p>\n<\/li>\n<li>\n<p>TypeScript-\u0442\u0438\u043f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430 \u0441\u0443\u0436\u0430\u0435\u0442\u0441\u044f \u0434\u043e \u044d\u0442\u0438\u0445 \u043f\u043e\u043b\u0435\u0439.<\/p>\n<\/li>\n<\/ul>\n<p>\u0415\u0441\u043b\u0438 \u0432\u044b\u0431\u0440\u0430\u0442\u044c <code>projected('public')<\/code>, \u0442\u043e \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c\u0441\u044f \u043a <code>user.passwordHash<\/code> \u0443\u0436\u0435 \u043d\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0441\u044f \u043d\u0430 \u044d\u0442\u0430\u043f\u0435 \u043a\u043e\u043c\u043f\u0438\u043b\u044f\u0446\u0438\u0438. \u042d\u0442\u043e \u0438\u043c\u0435\u043d\u043d\u043e \u0442\u0430 \u043c\u0435\u043b\u043e\u0447\u044c, \u0440\u0430\u0434\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0438 \u0445\u043e\u0447\u0435\u0442\u0441\u044f \u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0441\u0445\u0435\u043c\u0443 \u043a\u0430\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u043f\u0440\u0430\u0432\u0434\u044b, \u0430 \u043d\u0435 \u043a\u0430\u043a \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u0441 \u0442\u0438\u043f\u0430\u043c\u0438 \u0440\u044f\u0434\u043e\u043c \u0441 SQL.<\/p>\n<h3>Foreign keys \u0438 \u0438\u043d\u0434\u0435\u043a\u0441\u044b<\/h3>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0430 \u0434\u043e\u043c\u0435\u043d\u043d\u0443\u044e \u0447\u0430\u0441\u0442\u044c xpenser. \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438, \u043f\u0440\u043e\u0434\u0430\u0432\u0446\u043e\u0432 \u0438 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438. \u0422\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044f \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0438\u0442 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044e, \u0438\u043c\u0435\u0435\u0442 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044e, \u043e\u043f\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0434\u0430\u0432\u0446\u0430, \u0432\u0430\u043b\u044e\u0442\u0443, \u0441\u0443\u043c\u043c\u0443, \u0434\u0430\u0442\u0443 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438 \u0438 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u0430\u043d\u043d\u0443\u044e \u0441\u0443\u043c\u043c\u0443 \u0432 \u0432\u0430\u043b\u044e\u0442\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f:<\/p>\n<pre><code class=\"typescript\">export const TransactionDbSchema = object({    id: number().primaryKey(),    userId: number()        .hasColumnName('user_id')        .references('users', 'id')        .onDelete('CASCADE')        .index('idx_transactions_user_id'),    categoryId: number()        .hasColumnName('category_id')        .references('categories', 'id')        .onDelete('RESTRICT')        .index('idx_transactions_category_id'),    vendorId: number()        .hasColumnName('vendor_id')        .references('vendors', 'id')        .onDelete('SET NULL')        .index('idx_transactions_vendor_id')        .optional(),    type: string(),    amount: number(),    currency: string(),    defaultCurrencyAmount: number().hasColumnName('default_currency_amount'),    defaultCurrency: string().hasColumnName('default_currency'),    exchangeRate: number().hasColumnName('exchange_rate'),    exchangeRateDate: string().hasColumnName('exchange_rate_date'),    occurredAt: date().hasColumnName('occurred_at'),    note: string().optional(),    createdAt: date().hasColumnName('created_at').defaultTo('now'),    updatedAt: date().hasColumnName('updated_at').defaultTo('now'),    category: CategoryDbSchema.optional()}).hasTableName('transactions');<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041c\u0435\u0442\u043e\u0434\u044b <code>.references()<\/code>, <code>.onDelete()<\/code> \u0438 <code>.index()<\/code> \u043d\u0443\u0436\u043d\u044b \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0438\u0442\u0430\u0431\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438. \u041e\u043d\u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442\u0441\u044f \u0432 introspection metadata \u0441\u0445\u0435\u043c\u044b, \u0430 \u0437\u043d\u0430\u0447\u0438\u0442 \u0438\u0445 \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043b\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 DDL, \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u044f \u0441\u0445\u0435\u043c \u0438 \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0439.<\/p>\n<p>\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 <code>category: CategoryDbSchema.optional()<\/code>. \u042d\u0442\u043e \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u043e: \u043e\u043d\u043e \u043d\u0443\u0436\u043d\u043e ORM, \u0447\u0442\u043e\u0431\u044b \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 <code>.include(...)<\/code> \u0431\u044b\u043b \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c. \u0421\u0430\u043c\u0430 \u0441\u0432\u044f\u0437\u044c \u043e\u0431\u044a\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e, \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 entity.<\/p>\n<h3>Entity \u0438 relations<\/h3>\n<p>\u0421\u0445\u0435\u043c\u0430 \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 \u0444\u043e\u0440\u043c\u0443 \u0441\u0442\u0440\u043e\u043a\u0438 \u0438 DB-\u043c\u0435\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u0435. Entity \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u0432\u044f\u0437\u0438:<\/p>\n<pre><code class=\"typescript\">export const CategoryEntity = defineEntity(CategoryDbSchema);export const VendorEntity = defineEntity(VendorDbSchema);export const TransactionEntity = defineEntity(TransactionDbSchema).belongsTo(    t =&gt; t.category,    l =&gt; l.categoryId,    r =&gt; r.id);<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0412 \u044d\u0442\u043e\u043c \u043f\u0440\u0438\u043c\u0435\u0440\u0435:<\/p>\n<ul>\n<li>\n<p><code>t =&gt; t.category<\/code> \u2014 \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0435;<\/p>\n<\/li>\n<li>\n<p><code>l =&gt; l.categoryId<\/code> \u2014 FK \u043d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0435 <code>transactions<\/code>;<\/p>\n<\/li>\n<li>\n<p><code>r =&gt; <\/code><a href=\"http:\/\/r.id\" rel=\"noopener noreferrer nofollow\"><code>r.id<\/code><\/a> \u2014 PK \u043d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0435 <code>categories<\/code>.<\/p>\n<\/li>\n<\/ul>\n<p>\u0412\u0441\u0435 \u0442\u0440\u0438 \u0432\u044b\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0415\u0441\u043b\u0438 \u043f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u0442\u044c <code>categoryId<\/code> \u0438\u043b\u0438 \u043f\u043e\u043f\u044b\u0442\u0430\u0442\u044c\u0441\u044f \u0441\u0432\u044f\u0437\u0430\u0442\u044c \u0447\u0438\u0441\u043b\u043e \u0441\u043e \u0441\u0442\u0440\u043e\u043a\u043e\u0439, TypeScript \u043f\u043e\u043a\u0430\u0436\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0443 \u0434\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.<\/p>\n<p>\u0414\u043b\u044f \u0434\u0440\u0443\u0433\u0438\u0445 \u0441\u043b\u0443\u0447\u0430\u0435\u0432 \u0435\u0441\u0442\u044c <code>.hasOne()<\/code>, <code>.hasMany()<\/code> \u0438 <code>.belongsToMany()<\/code>. \u0412 xpenser \u043f\u043e\u043a\u0430 \u0445\u0432\u0430\u0442\u0430\u0435\u0442 <code>belongsTo<\/code>, \u043d\u043e \u0432 \u0442\u0435\u0441\u0442\u0430\u0445 ORM \u043f\u043e\u043a\u0440\u044b\u0442\u044b \u0438 \u0431\u043e\u043b\u0435\u0435 \u0441\u043b\u043e\u0436\u043d\u044b\u0435 \u0433\u0440\u0430\u0444\u044b: \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435 \u0434\u0435\u0440\u0435\u0432\u0430 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432, many-to-many \u0447\u0435\u0440\u0435\u0437 pivot \u0442\u0430\u0431\u043b\u0438\u0446\u0443, composite primary keys \u0438 polymorphic variants.<\/p>\n<h3>DbContext: \u043a\u0430\u0440\u0442\u0430 \u0432\u0441\u0435\u0445 \u0442\u0430\u0431\u043b\u0438\u0446<\/h3>\n<p>\u041f\u043e\u0441\u043b\u0435 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u044f entity \u0441\u043e\u0431\u0438\u0440\u0430\u044e\u0442\u0441\u044f \u0432 \u043e\u0431\u044b\u0447\u043d\u044b\u0439 \u043e\u0431\u044a\u0435\u043a\u0442:<\/p>\n<pre><code class=\"typescript\">export const entityMap = {    users: UserEntity,    categories: CategoryEntity,    vendors: VendorEntity,    transactions: TransactionEntity,    transactionScans: TransactionScanEntity,    transactionScanItems: TransactionScanItemEntity,    transactionScanImages: TransactionScanImageEntity,    exchangeRates: ExchangeRateEntity};export type AppEntityMap = typeof entityMap;export type AppDb = DbContext&lt;AppEntityMap&gt;;<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041d\u0430 \u0441\u0442\u0430\u0440\u0442\u0435 API \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0441\u044f <code>DbContext<\/code>:<\/p>\n<pre><code class=\"typescript\">import { createDb } from '@cleverbrush\/orm';import { instrumentKnex } from '@cleverbrush\/otel';import knex from 'knex';import { entityMap } from '..\/db\/schemas.js';const connection = instrumentKnex(    knex({        client: 'pg',        connection: config.db.connectionString,        pool: { min: 2, max: 10 },        acquireConnectionTimeout: 10_000    }),    { sanitizeStatement: () =&gt; '&lt;redacted&gt;' });const db = createDb(connection, entityMap);<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041d\u0430 \u0432\u044b\u0445\u043e\u0434\u0435 <code>db.users<\/code>, <code>db.categories<\/code>, <code>db.transactions<\/code> \u0438 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u043e\u043b\u044f \u0441\u0442\u0430\u043d\u043e\u0432\u044f\u0442\u0441\u044f \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 <code>DbSet<\/code>. \u042d\u0442\u043e \u043f\u043e\u0445\u043e\u0436\u0435 \u043d\u0430 <code>DbContext<\/code> \u0438\u0437 Entity Framework, \u0442\u043e\u043b\u044c\u043a\u043e \u0431\u0435\u0437 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u043e\u0432, reflection metadata \u0438 \u043a\u043e\u0434\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438.<\/p>\n<h3>\u0417\u0430\u043f\u0440\u043e\u0441\u044b: \u043e\u0442 \u043f\u0440\u043e\u0441\u0442\u043e\u0433\u043e \u043a \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c\u0443<\/h3>\n<p>\u0421\u0430\u043c\u044b\u0439 \u043f\u0440\u043e\u0441\u0442\u043e\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a:<\/p>\n<pre><code class=\"typescript\">const categories = await db.categories.where(    category =&gt; category.userId,    userId);<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041a\u043e\u043b\u043e\u043d\u043a\u0430 \u0432\u044b\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437 selector. \u0412 TypeScript \u044d\u0442\u043e \u043f\u043e\u043b\u0435 \u043d\u0430\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f <code>userId<\/code>, \u0430 \u0432 SQL \u0443\u0439\u0434\u0451\u0442 <code>user_id<\/code>.<\/p>\n<p>\u041c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u043e\u0434\u043d\u0443 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044e \u0432\u043c\u0435\u0441\u0442\u0435 \u0441 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0435\u0439:<\/p>\n<pre><code class=\"typescript\">const row = await db.transactions    .include(transaction =&gt; transaction.category)    .where(transaction =&gt; transaction.id, transactionId)    .where(transaction =&gt; transaction.userId, userId)    .first();<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>include(transaction =&gt; transaction.category)<\/code> \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u0442\u043e\u043c\u0443, \u0447\u0442\u043e \u0441\u0432\u044f\u0437\u044c \u0431\u044b\u043b\u0430 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0430 \u0432 <code>TransactionEntity<\/code>. \u0420\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 \u0437\u043d\u0430\u0435\u0442 \u043e \u043f\u043e\u043b\u0435 <code>category<\/code>, \u0438 \u044d\u0442\u043e \u043f\u043e\u043b\u0435 \u0438\u043c\u0435\u0435\u0442 \u0442\u0438\u043f \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438, \u0430 \u043d\u0435 <code>unknown<\/code>.<\/p>\n<p>\u0412 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u043f\u043e\u0441\u0442\u0435\u043f\u0435\u043d\u043d\u043e \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442\u0441\u044f \u0438\u0437 \u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432:<\/p>\n<pre><code class=\"typescript\">let builder = db.transactions    .include(transaction =&gt; transaction.category)    .where(transaction =&gt; transaction.userId, userId);if (query.categoryId) {    builder = builder.where(        transaction =&gt; transaction.categoryId,        query.categoryId    );}if (query.from) {    builder = builder.where(        transaction =&gt; transaction.occurredAt,        '&gt;=',        query.from    );}if (query.to) {    builder = builder.where(        transaction =&gt; transaction.occurredAt,        '&lt;=',        query.to    );}const rows = await builder    .orderBy(transaction =&gt; transaction.occurredAt, 'desc')    .orderBy(transaction =&gt; transaction.id, 'desc');<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u042d\u0442\u043e \u0432\u0441\u0451 \u0435\u0449\u0451 Knex-\u043f\u043e\u0434\u043e\u0431\u043d\u044b\u0439 fluent API, \u043d\u043e \u0431\u0435\u0437 \u0441\u0442\u0440\u043e\u043a\u043e\u0432\u044b\u0445 \u0438\u043c\u0451\u043d \u043a\u043e\u043b\u043e\u043d\u043e\u043a \u0432 \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u043c \u043f\u0443\u0442\u0438.<\/p>\n<h3>\u0412\u0441\u0442\u0430\u0432\u043a\u0430, \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435, \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435<\/h3>\n<p>\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438 \u0432 xpenser \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a:<\/p>\n<pre><code class=\"typescript\">const created = await db.transactions.insert({    userId,    categoryId: body.categoryId,    vendorId: body.vendorId ?? undefined,    type: category.type,    amount: body.amount,    currency: body.currency,    defaultCurrencyAmount: convertAmount(body.amount, exchange.rate),    defaultCurrency: user.defaultCurrency,    exchangeRate: exchange.rate,    exchangeRateDate: exchange.rateDate,    occurredAt: body.occurredAt,    note: body.note ?? undefined});<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0422\u0438\u043f payload \u0432\u044b\u0432\u043e\u0434\u0438\u0442\u0441\u044f \u0438\u0437 \u0441\u0445\u0435\u043c\u044b. \u041d\u0435\u043b\u044c\u0437\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0441\u0442\u0440\u043e\u043a\u0443 \u0432 <code>amount<\/code>, \u043d\u0435\u043b\u044c\u0437\u044f \u0437\u0430\u0431\u044b\u0442\u044c \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 <code>categoryId<\/code>, \u043d\u0435\u043b\u044c\u0437\u044f \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c <code>default_currency_amount<\/code> \u0432\u043c\u0435\u0441\u0442\u043e <code>defaultCurrencyAmount<\/code>.<\/p>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435:<\/p>\n<pre><code class=\"typescript\">await db.transactions    .where(transaction =&gt; transaction.id, transactionId)    .where(transaction =&gt; transaction.userId, userId)    .update({        categoryId: next.categoryId,        vendorId: (next.vendorId ?? null) as never,        type: category.type,        amount: next.amount,        currency: next.currency,        defaultCurrencyAmount: convertAmount(next.amount, exchange.rate),        defaultCurrency: user.defaultCurrency,        exchangeRate: exchange.rate,        exchangeRateDate: exchange.rateDate,        occurredAt: next.occurredAt,        note: next.note ?? undefined,        updatedAt: new Date()    });<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435:<\/p>\n<pre><code class=\"typescript\">const deleted = await db.transactions    .where(transaction =&gt; transaction.id, transactionId)    .where(transaction =&gt; transaction.userId, userId)    .delete();if (deleted === 0) {    throw new TransactionNotFoundError('Transaction was not found.');}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u0440\u0438 \u044d\u0442\u043e\u043c escape hatch \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f. \u0412 xpenser \u0435\u0441\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441\u044b, \u0433\u0434\u0435 \u043f\u0440\u043e\u0449\u0435 \u0432\u0437\u044f\u0442\u044c \u0447\u0438\u0441\u0442\u044b\u0439 Knex: \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043a\u043e\u0433\u0434\u0430 \u043d\u0443\u0436\u043d\u0430 \u0440\u0443\u0447\u043d\u0430\u044f \u0432\u044b\u0431\u043e\u0440\u043a\u0430 \u0438\u0437 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u0442\u0430\u0431\u043b\u0438\u0446, \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0435 aliases \u0438\u043b\u0438 SQL, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0435 \u0445\u043e\u0447\u0435\u0442\u0441\u044f \u043f\u0440\u044f\u0442\u0430\u0442\u044c \u0437\u0430 ORM API. \u042d\u0442\u043e \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e: ORM \u043d\u0435 \u0434\u043e\u043b\u0436\u0435\u043d \u043c\u0435\u0448\u0430\u0442\u044c \u043f\u0438\u0441\u0430\u0442\u044c SQL \u0442\u0430\u043c, \u0433\u0434\u0435 SQL \u043e\u0447\u0435\u0432\u0438\u0434\u043d\u0435\u0435.<\/p>\n<h3>\u0422\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438<\/h3>\n<p><code>DbContext<\/code> \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 callback-\u0444\u043e\u0440\u043c\u0443 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0439:<\/p>\n<pre><code class=\"typescript\">await db.transaction(async trx =&gt; {    await trx.telegramLinkTokens        .where(candidate =&gt; candidate.userId, userId)        .delete();    await trx.telegramLinkTokens.insert({        userId,        tokenHash: hashTelegramLinkToken(token),        expiresAt,        consumedAt: undefined    });});<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0415\u0441\u043b\u0438 callback \u0431\u0440\u043e\u0441\u0438\u0442 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435, Knex \u043e\u0442\u043a\u0430\u0442\u0438\u0442 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044e. \u0412\u043d\u0443\u0442\u0440\u0438 callback \u0432\u044b \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0435 \u0442\u043e\u0442 \u0436\u0435 <code>DbContext<\/code>, \u043d\u043e \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0439 \u043a <code>trx<\/code>, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 <code>trx.users<\/code>, <code>trx.transactions<\/code>, <code>trx.telegramLinkTokens<\/code> \u043e\u0441\u0442\u0430\u044e\u0442\u0441\u044f \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 <code>DbSet<\/code>.<\/p>\n<p>\u041f\u0440\u0438\u043c\u0435\u0440 \u043f\u043e\u0441\u043b\u043e\u0436\u043d\u0435\u0435: \u043f\u0435\u0440\u0435\u0434 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438 xpenser \u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0438\u0442 \u0432\u0441\u0435 \u0435\u0451 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438 \u0432 replacement-\u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044e \u0438 \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e \u0443\u0434\u0430\u043b\u044f\u0435\u0442 \u0441\u0442\u0430\u0440\u0443\u044e:<\/p>\n<pre><code class=\"typescript\">await db.transaction(async trx =&gt; {    await trx.transactions        .where(transaction =&gt; transaction.userId, userId)        .where(transaction =&gt; transaction.categoryId, categoryId)        .update({            categoryId: replacement.id,            type: replacement.type,            updatedAt: now        });    await trx.categories        .where(candidate =&gt; candidate.id, categoryId)        .where(candidate =&gt; candidate.userId, userId)        .delete();});<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0422\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u043e\u043d\u043d\u0430\u044f \u0433\u0440\u0430\u043d\u0438\u0446\u0430 \u0432\u0438\u0434\u043d\u0430 \u0441\u0440\u0430\u0437\u0443, \u0430 \u0442\u0438\u043f\u0438\u0437\u0430\u0446\u0438\u044f \u043d\u0438\u043a\u0443\u0434\u0430 \u043d\u0435 \u043f\u0440\u043e\u043f\u0430\u0434\u0430\u0435\u0442.<\/p>\n<h3>\u041c\u0438\u0433\u0440\u0430\u0446\u0438\u0438<\/h3>\n<p>\u0423 <code>@cleverbrush\/knex-schema<\/code> \u0435\u0441\u0442\u044c snapshot-based \u0433\u0435\u043d\u0435\u0440\u0430\u0442\u043e\u0440 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0439:<\/p>\n<pre><code class=\"typescript\">import {    generateMigrationsForContext,    loadSnapshot,    writeSnapshot} from '@cleverbrush\/knex-schema';const prev = loadSnapshot('.\/migrations\/snapshot.json');const result = generateMigrationsForContext(    Object.values(entityMap),    prev);if (!result.isEmpty) {    await fs.promises.writeFile(        '.\/migrations\/20260611000000_changes.ts',        result.full    );    writeSnapshot('.\/migrations\/snapshot.json', result.nextSnapshot);}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0424\u0443\u043d\u043a\u0446\u0438\u044f \u0441\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u0435\u0442 \u0442\u0435\u043a\u0443\u0449\u0438\u0435 entity-\u0441\u0445\u0435\u043c\u044b \u0441 \u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u0441\u043d\u0438\u043c\u043a\u043e\u043c \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0435\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f:<\/p>\n<ul>\n<li>\n<p>\u0434\u043b\u044f \u043d\u043e\u0432\u044b\u0445 \u0442\u0430\u0431\u043b\u0438\u0446 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 <code>CREATE TABLE<\/code>;<\/p>\n<\/li>\n<li>\n<p>\u0434\u043b\u044f \u0438\u0437\u043c\u0435\u043d\u0451\u043d\u043d\u044b\u0445 \u0442\u0430\u0431\u043b\u0438\u0446 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 <code>ALTER TABLE<\/code>;<\/p>\n<\/li>\n<li>\n<p>\u0434\u043b\u044f \u0443\u0434\u0430\u043b\u0451\u043d\u043d\u044b\u0445 entity \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0443\u0435\u0442 <code>DROP TABLE<\/code>;<\/p>\n<\/li>\n<li>\n<p>\u0441\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u0442 \u0442\u0430\u0431\u043b\u0438\u0446\u044b \u043f\u043e FK-\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u044f\u043c, \u0447\u0442\u043e\u0431\u044b \u0440\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0435 \u0442\u0430\u0431\u043b\u0438\u0446\u044b \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u043b\u0438\u0441\u044c \u0440\u0430\u043d\u044c\u0448\u0435 \u0434\u043e\u0447\u0435\u0440\u043d\u0438\u0445.<\/p>\n<\/li>\n<\/ul>\n<p>\u0412\u0430\u0436\u043d\u0430\u044f \u043e\u0433\u043e\u0432\u043e\u0440\u043a\u0430: xpenser \u0441\u0435\u0439\u0447\u0430\u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043e\u0431\u044b\u0447\u043d\u044b\u0435 handwritten Knex migrations \u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442 \u0438\u0445 \u0447\u0435\u0440\u0435\u0437 <code>knex.migrate.latest(...)<\/code>:<\/p>\n<pre><code class=\"typescript\">export async function runMigrations(knex: Knex): Promise&lt;void&gt; {    await knex.migrate.latest({        directory: migrationsDirectory,        tableName: 'knex_migrations',        loadExtensions: ['.ts', '.js']    });}<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0422\u043e \u0435\u0441\u0442\u044c \u0433\u0435\u043d\u0435\u0440\u0430\u0442\u043e\u0440 \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0439 \u2014 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c framework-\u043f\u0430\u043a\u0435\u0442\u0430, \u0430 \u043d\u0435 \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u0443\u0442\u044c. \u041c\u043d\u0435 \u043d\u0440\u0430\u0432\u0438\u0442\u0441\u044f \u0438\u043c\u0435\u0442\u044c \u043e\u0431\u0430 \u0432\u0430\u0440\u0438\u0430\u043d\u0442\u0430: \u043f\u0440\u043e\u0441\u0442\u044b\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0438\u0437 diff \u0441\u0445\u0435\u043c, \u0430 \u0441\u043b\u043e\u0436\u043d\u044b\u0435 data migrations \u0432\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u043f\u0438\u0441\u0430\u0442\u044c \u0440\u0443\u043a\u0430\u043c\u0438 \u043d\u0430 Knex.<\/p>\n<h3>\u041d\u0438\u0436\u043d\u0438\u0439 \u0443\u0440\u043e\u0432\u0435\u043d\u044c: query(knex, schema)<\/h3>\n<p><code>@cleverbrush\/orm<\/code> \u2014 \u043d\u0435 \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0432\u0445\u043e\u0434. \u0415\u0441\u043b\u0438 \u043d\u0435 \u043d\u0443\u0436\u0435\u043d <code>DbContext<\/code>, \u043c\u043e\u0436\u043d\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u043f\u0440\u044f\u043c\u043e \u0441\u043e \u0441\u0445\u0435\u043c\u043e\u0439:<\/p>\n<pre><code class=\"typescript\">import { query } from '@cleverbrush\/knex-schema';const users = await query(knex, UserDbSchema)    .where(user =&gt; user.emailVerified, true)    .projected('public');<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u042d\u0442\u043e\u0442 \u0441\u043b\u043e\u0439 \u0434\u0430\u0451\u0442 \u0442\u043e\u0442 \u0436\u0435 \u043f\u0435\u0440\u0435\u0432\u043e\u0434 \u0438\u043c\u0451\u043d \u043a\u043e\u043b\u043e\u043d\u043e\u043a, projections, scopes, pagination, insert\/update\/delete, <code>joinOne<\/code>, <code>joinMany<\/code>, raw escape hatches \u0438 DDL helpers. ORM \u043f\u0440\u043e\u0441\u0442\u043e \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043f\u043e\u0432\u0435\u0440\u0445 \u043d\u0435\u0433\u043e entity map, relations, <code>DbSet<\/code>, \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442 \u0438 tracking.<\/p>\n<h3>Tracking context \u0438 unit-of-work<\/h3>\n<p>\u0414\u043b\u044f \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0435\u0432 \u0432 \u0441\u0442\u0438\u043b\u0435 \u00ab\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442, \u043f\u043e\u043c\u0435\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0432\u043e\u0439\u0441\u0442\u0432, \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f\u00bb \u0435\u0441\u0442\u044c tracking mode:<\/p>\n<pre><code class=\"typescript\">await using db = createDb(knex, entityMap, { tracking: true });const user = await db.users.findOrFail(userId);user.defaultCurrency = 'EUR';user.updatedAt = new Date();const result = await db.saveChanges();<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>Tracking context \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 identity map: \u0435\u0441\u043b\u0438 \u043e\u0434\u0438\u043d \u0438 \u0442\u043e\u0442 \u0436\u0435 primary key \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d \u0434\u0432\u0430\u0436\u0434\u044b, \u0432\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0435 \u043e\u0434\u0438\u043d \u0438 \u0442\u043e\u0442 \u0436\u0435 object reference. \u041f\u0440\u0438 <code>saveChanges()<\/code> ORM \u0441\u0440\u0430\u0432\u043d\u0438\u0442 \u0442\u0435\u043a\u0443\u0449\u0438\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0441\u043e snapshot \u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442 \u0432 \u0411\u0414 \u0442\u043e\u043b\u044c\u043a\u043e \u0438\u0437\u043c\u0435\u043d\u0451\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044f.<\/p>\n<p>\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u044b \u0438 \u0431\u043e\u043b\u0435\u0435 \u044f\u0432\u043d\u044b\u0435 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438:<\/p>\n<pre><code class=\"typescript\">db.attach('users', existingUser);const entry = db.entry(existingUser);entry.isModified('defaultCurrency');db.remove(existingUser);await db.saveChanges();<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u0435\u0440\u0435\u0434 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435\u043c \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u0432\u0435\u0441\u0438\u0442\u044c hook, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0434\u043b\u044f audit-\u043f\u043e\u043b\u0435\u0439:<\/p>\n<pre><code class=\"typescript\">db.onSavingChanges(entry =&gt; {    if (entry.state === 'Modified' &amp;&amp; 'updatedAt' in entry.entity) {        (entry.entity as { updatedAt: Date }).updatedAt = new Date();    }});<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p><code>await using<\/code> \u0437\u0434\u0435\u0441\u044c \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043a\u0430\u043a runtime guard. \u041f\u0440\u0438 \u0432\u044b\u0445\u043e\u0434\u0435 \u0438\u0437 \u0431\u043b\u043e\u043a\u0430 \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f <code>[Symbol.asyncDispose]()<\/code>: \u0435\u0441\u043b\u0438 \u043e\u0441\u0442\u0430\u043b\u0438\u0441\u044c \u043d\u0435\u0441\u043e\u0445\u0440\u0430\u043d\u0451\u043d\u043d\u044b\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f, \u0431\u0443\u0434\u0435\u0442 \u0431\u0440\u043e\u0448\u0435\u043d <code>PendingChangesError<\/code>. \u042d\u0442\u043e \u043d\u0435 \u043c\u0430\u0433\u0438\u044f \u043a\u043e\u043c\u043f\u0438\u043b\u044f\u0442\u043e\u0440\u0430, \u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u0447\u043d\u0430\u044f \u0437\u0430\u0449\u0438\u0442\u0430 \u043e\u0442 \u0442\u0438\u0445\u043e\u0439 \u043f\u043e\u0442\u0435\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439.<\/p>\n<h3>Row versioning \u0438 \u043a\u043e\u043d\u043a\u0443\u0440\u0435\u043d\u0442\u043d\u044b\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f<\/h3>\n<p>\u0415\u0441\u043b\u0438 \u0443 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 \u0435\u0441\u0442\u044c \u0432\u0435\u0440\u0441\u0438\u044f \u0441\u0442\u0440\u043e\u043a\u0438, \u0435\u0451 \u043c\u043e\u0436\u043d\u043e \u044f\u0432\u043d\u043e \u043e\u0442\u043c\u0435\u0442\u0438\u0442\u044c:<\/p>\n<pre><code class=\"typescript\">const OrderSchema = object({    id: number().primaryKey(),    status: string(),    version: number().rowVersion()}).hasTableName('orders');<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u041f\u0440\u0438 <code>saveChanges()<\/code> ORM \u0434\u043e\u0431\u0430\u0432\u0438\u0442 \u0443\u0441\u043b\u043e\u0432\u0438\u0435 \u0432\u0438\u0434\u0430 <code>AND version = &lt;snapshot&gt;<\/code>. \u0415\u0441\u043b\u0438 \u0434\u0440\u0443\u0433\u0430\u044f \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044f \u0443\u0436\u0435 \u043e\u0431\u043d\u043e\u0432\u0438\u043b\u0430 \u0441\u0442\u0440\u043e\u043a\u0443 \u0438 \u0432\u0435\u0440\u0441\u0438\u044f \u0438\u0437\u043c\u0435\u043d\u0438\u043b\u0430\u0441\u044c, <code>UPDATE<\/code> \u043d\u0435 \u0437\u0430\u0442\u0440\u043e\u043d\u0435\u0442 \u043d\u0438 \u043e\u0434\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u0438, \u0430 ORM \u0431\u0440\u043e\u0441\u0438\u0442 <code>ConcurrencyError<\/code>.<\/p>\n<p>\u0412 xpenser \u044d\u0442\u0430 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043f\u043e\u043a\u0430 \u043d\u0435 \u043d\u0443\u0436\u043d\u0430, \u043d\u043e \u0434\u043b\u044f \u0441\u0438\u0441\u0442\u0435\u043c \u0441 \u0441\u043e\u0432\u043c\u0435\u0441\u0442\u043d\u044b\u043c \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043e\u043d\u0430 \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442 \u0442\u0438\u043f\u0438\u0447\u043d\u044b\u0439 optimistic concurrency \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0439.<\/p>\n<h3>\u0427\u0442\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u043e\u0441\u044c<\/h3>\n<p>\u0415\u0441\u043b\u0438 \u0441\u043e\u0431\u0440\u0430\u0442\u044c \u0432\u0441\u0451 \u0432\u043c\u0435\u0441\u0442\u0435, \u0446\u0435\u043f\u043e\u0447\u043a\u0430 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a:<\/p>\n<pre><code class=\"typescript\">@cleverbrush\/schema    -&gt; runtime validation    -&gt; TypeScript inference    -&gt; PropertyDescriptors    -&gt; DB metadata via @cleverbrush\/knex-schema    -&gt; typed queries and DDL helpers    -&gt; DbContext \/ DbSet via @cleverbrush\/orm<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<p>\u0413\u043b\u0430\u0432\u043d\u0430\u044f \u0446\u0435\u043d\u043d\u043e\u0441\u0442\u044c \u0437\u0434\u0435\u0441\u044c \u043d\u0435 \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u043f\u0438\u0441\u0430\u0442\u044c \u043c\u0435\u043d\u044c\u0448\u0435 SQL. SQL \u0432\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u0432\u0430\u0436\u043d\u044b\u043c, \u0430 Knex \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c. \u0426\u0435\u043d\u043d\u043e\u0441\u0442\u044c \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e \u0433\u0440\u0430\u043d\u0438\u0446\u044b \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u043d\u0430\u0447\u0438\u043d\u0430\u044e\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c\u0441\u044f \u043a\u043e\u043c\u043f\u0438\u043b\u044f\u0442\u043e\u0440\u043e\u043c:<\/p>\n<ul>\n<li>\n<p>\u043f\u043e\u043b\u0435 \u043f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u043e \u0432 \u0441\u0445\u0435\u043c\u0435 \u2014 selectors \u0432 \u0437\u0430\u043f\u0440\u043e\u0441\u0430\u0445 \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u044e\u0442 \u043a\u043e\u043c\u043f\u0438\u043b\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f;<\/p>\n<\/li>\n<li>\n<p>\u043a\u043e\u043b\u043e\u043d\u043a\u0430 \u043d\u0430\u0437\u044b\u0432\u0430\u0435\u0442\u0441\u044f <code>default_currency_amount<\/code>, \u043d\u043e \u0432 \u043a\u043e\u0434\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f <code>defaultCurrencyAmount<\/code>;<\/p>\n<\/li>\n<li>\n<p>projection \u0441\u0443\u0436\u0430\u0435\u0442 \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e SQL <code>SELECT<\/code>, \u043d\u043e \u0438 TypeScript-\u0442\u0438\u043f;<\/p>\n<\/li>\n<li>\n<p>relation \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u0438 \u0434\u0430\u043b\u044c\u0448\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437 <code>.include(t =&gt; t.category)<\/code>;<\/p>\n<\/li>\n<li>\n<p>\u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u043e\u043d\u043d\u044b\u0439 <code>DbContext<\/code> \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u0442\u043e\u0442 \u0436\u0435 API \u0432\u043d\u0443\u0442\u0440\u0438 <code>trx<\/code>.<\/p>\n<\/li>\n<\/ul>\n<h3>\u0427\u0435\u0433\u043e \u0437\u0434\u0435\u0441\u044c \u043d\u0435\u0442<\/h3>\n<p>\u042d\u0442\u043e \u043d\u0435 \u0437\u0430\u043c\u0435\u043d\u0430 Prisma, TypeORM \u0438\u043b\u0438 Drizzle. \u0423 \u044d\u0442\u0438\u0445 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u043e\u0432 \u0441\u0432\u043e\u0438 \u0441\u0438\u043b\u044c\u043d\u044b\u0435 \u0441\u0442\u043e\u0440\u043e\u043d\u044b: \u0437\u0440\u0435\u043b\u0430\u044f \u044d\u043a\u043e\u0441\u0438\u0441\u0442\u0435\u043c\u0430, schema language, studio, generators, \u0430\u0434\u0430\u043f\u0442\u0435\u0440\u044b, \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f, \u043f\u0440\u0438\u0432\u044b\u0447\u043d\u044b\u0435 \u043f\u0430\u0442\u0442\u0435\u0440\u043d\u044b.<\/p>\n<p><code>@cleverbrush\/orm<\/code> \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u0435\u043d \u0432 \u0434\u0440\u0443\u0433\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435: \u043a\u043e\u0433\u0434\u0430 \u0443 \u0432\u0430\u0441 \u0443\u0436\u0435 \u0435\u0441\u0442\u044c schema-first \u0441\u0442\u0435\u043a, \u0438 \u0432\u044b \u0445\u043e\u0442\u0438\u0442\u0435, \u0447\u0442\u043e\u0431\u044b HTTP-\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u044b, \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f, \u0444\u043e\u0440\u043c\u044b, \u043b\u043e\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 database layer \u0433\u043e\u0432\u043e\u0440\u0438\u043b\u0438 \u043d\u0430 \u043e\u0434\u043d\u043e\u043c \u044f\u0437\u044b\u043a\u0435.<\/p>\n<p>\u0412 xpenser \u044d\u0442\u043e \u043e\u043a\u0430\u0437\u0430\u043b\u043e\u0441\u044c \u0443\u0434\u043e\u0431\u043d\u044b\u043c \u043a\u043e\u043c\u043f\u0440\u043e\u043c\u0438\u0441\u0441\u043e\u043c. \u0411\u043e\u043b\u044c\u0448\u0430\u044f \u0447\u0430\u0441\u0442\u044c CRUD \u0438 domain queries \u0436\u0438\u0432\u0451\u0442 \u043d\u0430 <code>DbSet<\/code>, \u0430 \u043d\u0435\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043e\u0441\u0442\u0430\u044e\u0442\u0441\u044f \u043d\u0430 \u043e\u0431\u044b\u0447\u043d\u043e\u043c Knex. \u041f\u0440\u0438 \u044d\u0442\u043e\u043c OpenTelemetry-\u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u0432\u0438\u0434\u0438\u0442 \u0438 \u0442\u043e, \u0438 \u0434\u0440\u0443\u0433\u043e\u0435, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u0432\u043d\u0438\u0437\u0443 \u043e\u0434\u0438\u043d \u0438 \u0442\u043e\u0442 \u0436\u0435 Knex connection pool.<\/p>\n<h3>\u0418\u0442\u043e\u0433\u0438<\/h3>\n<p><code>@cleverbrush\/knex-schema<\/code> \u0438 <code>@cleverbrush\/orm<\/code> \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u044e\u0442 \u043a Knex schema-driven \u0441\u043b\u043e\u0439:<\/p>\n<ul>\n<li>\n<p>\u0441\u0445\u0435\u043c\u044b \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u044e\u0442 TypeScript-\u0442\u0438\u043f, DB-\u0438\u043c\u0435\u043d\u0430, defaults, indexes \u0438 foreign keys;<\/p>\n<\/li>\n<li>\n<p>selectors \u0432\u0438\u0434\u0430 <code>t =&gt; t.userId<\/code> \u0437\u0430\u043c\u0435\u043d\u044f\u044e\u0442 \u0441\u0442\u0440\u043e\u043a\u043e\u0432\u044b\u0435 \u0438\u043c\u0435\u043d\u0430 \u043a\u043e\u043b\u043e\u043d\u043e\u043a;<\/p>\n<\/li>\n<li>\n<p>projections \u0441\u0443\u0436\u0430\u044e\u0442 \u0438 SQL, \u0438 TypeScript-\u0442\u0438\u043f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430;<\/p>\n<\/li>\n<li>\n<p><code>defineEntity()<\/code> \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 relations \u0438 typed eager loading;<\/p>\n<\/li>\n<li>\n<p><code>createDb()<\/code> \u043f\u0440\u0435\u0432\u0440\u0430\u0449\u0430\u0435\u0442 entity map \u0432 <code>DbContext<\/code> \u0441 <code>DbSet<\/code>;<\/p>\n<\/li>\n<li>\n<p>\u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442 \u0442\u043e\u0442 \u0436\u0435 \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 API;<\/p>\n<\/li>\n<li>\n<p>tracking context \u0434\u0430\u0451\u0442 identity map, <code>saveChanges()<\/code> \u0438 runtime guard \u043e\u0442 \u0437\u0430\u0431\u044b\u0442\u044b\u0445 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439;<\/p>\n<\/li>\n<li>\n<p>Knex \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u043c \u0434\u043b\u044f raw SQL \u0438 \u0441\u043b\u043e\u0436\u043d\u044b\u0445 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432.<\/p>\n<\/li>\n<\/ul>\n<p>xpenser \u043f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u044d\u0442\u0443 \u0438\u0434\u0435\u044e \u043d\u0430 \u0440\u0430\u0431\u043e\u0447\u0435\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438, \u0430 \u043d\u0435 \u043d\u0430 \u0438\u0441\u043a\u0443\u0441\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u043c todo-\u043f\u0440\u0438\u043c\u0435\u0440\u0435: \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438, \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438, \u043f\u0440\u043e\u0434\u0430\u0432\u0446\u044b, \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438, \u043a\u0443\u0440\u0441\u044b \u0432\u0430\u043b\u044e\u0442, \u0441\u043a\u0430\u043d\u044b \u0447\u0435\u043a\u043e\u0432, API keys, Telegram linking \u0438 MCP OAuth \u0436\u0438\u0432\u0443\u0442 \u0432 \u043e\u0434\u043d\u043e\u0439 PostgreSQL-\u043c\u043e\u0434\u0435\u043b\u0438.<\/p>\n<p>\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u0445\u043e\u0447\u0443 \u0443\u0436\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e \u0440\u0430\u0437\u043e\u0431\u0440\u0430\u0442\u044c \u0441\u0430\u043c xpenser: \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u043e\u0435\u043d\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0446\u0435\u043b\u0438\u043a\u043e\u043c, \u043f\u043e\u0447\u0435\u043c\u0443 \u044f \u0441\u0434\u0435\u043b\u0430\u043b \u0435\u0433\u043e \u043a\u0430\u043a open-source \u043b\u0438\u0447\u043d\u044b\u0439 \u0444\u0438\u043d\u0430\u043d\u0441\u043e\u0432\u044b\u0439 \u0442\u0440\u0435\u043a\u0435\u0440, \u043a\u0430\u043a \u0432 \u043d\u0451\u043c \u0441\u0432\u044f\u0437\u0430\u043d\u044b web app, API, Telegram bot, MCP endpoint, contracts, typed client, observability \u0438 database layer.<\/p>\n<h3>\u0421\u0441\u044b\u043b\u043a\u0438<\/h3>\n<p>Cleverbrush Framework: <a href=\"https:\/\/github.com\/cleverbrush\/framework\" rel=\"noopener noreferrer nofollow\">github.com\/cleverbrush\/framework<\/a><\/p>\n<p>xpenser app: <a href=\"http:\/\/xpenser.cleverbrush.com\" rel=\"noopener noreferrer nofollow\">xpenser.cleverbrush.com<\/a><\/p>\n<p>xpenser GitHub: <a href=\"https:\/\/github.com\/cleverbrush\/xpenser\" rel=\"noopener noreferrer nofollow\">github.com\/cleverbrush\/xpenser<\/a><\/p>\n<p>\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f \u0438 playground: <a href=\"http:\/\/docs.cleverbrush.com\" rel=\"noopener noreferrer nofollow\">docs.cleverbrush.com<\/a><\/p>\n<p>Knex: <a href=\"http:\/\/knexjs.org\" rel=\"noopener noreferrer nofollow\">knexjs.org<\/a><\/p>\n<h4>npm<\/h4>\n<pre><code class=\"bash\">npm install @cleverbrush\/knex-schemanpm install @cleverbrush\/orm knex pg<\/code><div class=\"code-explainer\"><a href=\"https:\/\/sourcecraft.dev\/\" class=\"tm-button code-explainer__link\" style=\"visibility: hidden;\"><img style=\"width:14px;height:14px;object-fit:cover;object-position:left;\"\/><\/a><\/div><\/pre>\n<\/div>\n<p>\u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/1046405\/\">https:\/\/habr.com\/ru\/articles\/1046405\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u0421\u0442\u0430\u0442\u044c\u044f \u043e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u0440\u0435\u0432\u0440\u0430\u0442\u0438\u0442\u044c TypeScript-\u0441\u0445\u0435\u043c\u0443 \u0432 \u0435\u0434\u0438\u043d\u044b\u0439 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u0438\u0441\u0442\u0438\u043d\u044b \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0431\u0430\u0437\u043e\u0439 \u0434\u0430\u043d\u043d\u044b\u0445: \u0438\u0437 \u043e\u0434\u043d\u043e\u0433\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u0438\u043f\u044b \u0441\u0442\u0440\u043e\u043a, \u0438\u043c\u0435\u043d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446 \u0438 \u043a\u043e\u043b\u043e\u043d\u043e\u043a, \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0437\u0430\u043f\u0440\u043e\u0441\u044b, \u0441\u0432\u044f\u0437\u0438, \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0438 \u0438 unit-of-work \u043f\u043e\u0432\u0435\u0440\u0445 Knex.\u0412\u0441\u0435 \u043f\u0440\u0438\u043c\u0435\u0440\u044b \u043d\u0438\u0436\u0435 \u0432\u0437\u044f\u0442\u044b \u0438\u0437 xpenser \u2014 open-source \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u0443\u0447\u0451\u0442\u0430 \u043b\u0438\u0447\u043d\u044b\u0445 \u0434\u043e\u0445\u043e\u0434\u043e\u0432 \u0438 \u0440\u0430\u0441\u0445\u043e\u0434\u043e\u0432. \u042d\u0442\u043e \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u043f\u043e\u043b\u0435\u0437\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435, \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u044f \u0441\u0430\u043c \u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0441\u044c, \u0438 \u0440\u0435\u0444\u0435\u0440\u0435\u043d\u0441\u043d\u0430\u044f \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f Cleverbrush Framework: \u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u044b, \u0441\u0435\u0440\u0432\u0435\u0440, \u043a\u043b\u0438\u0435\u043d\u0442, \u0444\u043e\u0440\u043c\u044b, auth, observability, PostgreSQL, Telegram bot \u0438 MCP endpoint \u0436\u0438\u0432\u0443\u0442 \u0432 \u043e\u0434\u043d\u043e\u043c \u0440\u0435\u043f\u043e\u0437\u0438\u0442\u043e\u0440\u0438\u0438. \u041a\u043e\u0434 \u043e\u0442\u043a\u0440\u044b\u0442: github.com\/cleverbrush\/xpenser.\u0414\u0438\u0441\u043a\u043b\u0435\u0439\u043c\u0435\u0440: \u0432\u0441\u0435 \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u043c\u044b\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u043d\u043e\u0441\u044f\u0442 \u044d\u043a\u0441\u043f\u0435\u0440\u0438\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0439 \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440. \u041d\u0435\u0441\u043c\u043e\u0442\u0440\u044f \u043d\u0430 \u044d\u0442\u043e, \u043f\u043e\u043a\u0440\u044b\u0442\u0438\u0435 \u0442\u0435\u0441\u0442\u0430\u043c\u0438 \u0443 \u043d\u0438\u0445 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u0445\u043e\u0440\u043e\u0448\u0435\u0435. \u0422\u0430\u043a\u0436\u0435 \u0432 \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0432 \u044d\u0442\u0438\u0445 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430\u0445 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043b\u0438\u0448\u044c Postgres \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0431\u0430\u0437 \u0434\u0430\u043d\u043d\u044b\u0445.\u041f\u0440\u0435\u0434\u044b\u0441\u0442\u043e\u0440\u0438\u044f\u0412 \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0445 \u0441\u0442\u0430\u0442\u044c\u044f\u0445 \u044f \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u044b\u0432\u0430\u043b \u043e @cleverbrush\/schema, \u043e \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c HTTP API \u0447\u0435\u0440\u0435\u0437 @cleverbrush\/server \u0438 @cleverbrush\/client, \u0430 \u0437\u0430\u0442\u0435\u043c \u043e @cleverbrush\/log \u0438 @cleverbrush\/otel.\u041e\u0431\u0449\u0430\u044f \u0438\u0434\u0435\u044f \u0443 \u0432\u0441\u0435\u0445 \u044d\u0442\u0438\u0445 \u043f\u0430\u043a\u0435\u0442\u043e\u0432 \u043e\u0434\u043d\u0430: \u0441\u0445\u0435\u043c\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u043d\u0435 \u043f\u0440\u043e\u0441\u0442\u043e \u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440\u043e\u043c, \u0430 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u043c \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438. \u0415\u0441\u043b\u0438 \u0441\u0445\u0435\u043c\u0430 \u0443\u0436\u0435 \u0437\u043d\u0430\u0435\u0442, \u0447\u0442\u043e \u0443 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0435\u0441\u0442\u044c \u043f\u043e\u043b\u0435 userId, \u0447\u0442\u043e \u044d\u0442\u043e \u0447\u0438\u0441\u043b\u043e, \u0447\u0442\u043e \u043e\u043d\u043e \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e, \u0438 \u0447\u0442\u043e \u0434\u043b\u044f \u043d\u0435\u0433\u043e \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c PropertyDescriptor \u0447\u0435\u0440\u0435\u0437 t =&gt; t.userId, \u0442\u043e \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u0435\u0442 \u0435\u0441\u0442\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0432\u043e\u043f\u0440\u043e\u0441: \u043f\u043e\u0447\u0435\u043c\u0443 \u0431\u044b \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e \u0436\u0435 \u0437\u043d\u0430\u043d\u0438\u0435 \u0434\u043b\u044f SQL?\u041d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0435 \u043e\u0442\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u0442\u0435\u0445, \u043a\u0442\u043e \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u043b \u0441 Knex. Knex \u2014 \u044d\u0442\u043e query builder \u0434\u043b\u044f Node.js: \u043e\u043d \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u0438\u0441\u0430\u0442\u044c SQL-\u0437\u0430\u043f\u0440\u043e\u0441\u044b \u0446\u0435\u043f\u043e\u0447\u043a\u0430\u043c\u0438 \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u0432\u043c\u0435\u0441\u0442\u043e \u0440\u0443\u0447\u043d\u043e\u0439 \u0441\u0431\u043e\u0440\u043a\u0438 \u0441\u0442\u0440\u043e\u043a, \u0443\u043c\u0435\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0441 PostgreSQL, MySQL, SQLite \u0438 \u0434\u0440\u0443\u0433\u0438\u043c\u0438 SQL-\u0431\u0430\u0437\u0430\u043c\u0438, \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438, connection pool, migrations \u0438 schema builder.\u041b\u044e\u0434\u0438 \u0447\u0430\u0441\u0442\u043e \u0432\u044b\u0431\u0438\u0440\u0430\u044e\u0442 Knex, \u043a\u043e\u0433\u0434\u0430 \u0445\u043e\u0442\u044f\u0442 \u043e\u0441\u0442\u0430\u0432\u0430\u0442\u044c\u0441\u044f \u0431\u043b\u0438\u0437\u043a\u043e \u043a SQL, \u043d\u043e \u043d\u0435 \u0445\u043e\u0442\u044f\u0442 \u043a\u0430\u0436\u0434\u044b\u0439 \u0440\u0430\u0437 \u0440\u0443\u043a\u0430\u043c\u0438 \u0441\u043e\u0431\u0438\u0440\u0430\u0442\u044c SELECT &#8230; WHERE &#8230; \u0438 \u0441\u043b\u0435\u0434\u0438\u0442\u044c \u0437\u0430 \u043f\u043b\u0435\u0439\u0441\u0445\u043e\u043b\u0434\u0435\u0440\u0430\u043c\u0438. \u042d\u0442\u043e \u0445\u043e\u0440\u043e\u0448\u0438\u0439 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043e\u0447\u043d\u044b\u0439 \u0441\u043b\u043e\u0439 \u043c\u0435\u0436\u0434\u0443 raw SQL \u0438 \u0442\u044f\u0436\u0451\u043b\u043e\u0439 ORM: \u043c\u0435\u043d\u044c\u0448\u0435 \u043c\u0430\u0433\u0438\u0438, \u043f\u0440\u043e\u0449\u0435 \u043f\u0440\u0435\u0434\u0441\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u0442\u043e\u0433\u043e\u0432\u044b\u0439 \u0437\u0430\u043f\u0440\u043e\u0441, \u043b\u0435\u0433\u043a\u043e \u0443\u043f\u0430\u0441\u0442\u044c \u043d\u0430 \u043e\u0431\u044b\u0447\u043d\u044b\u0439 SQL \u0442\u0430\u043c, \u0433\u0434\u0435 query builder \u043c\u0435\u0448\u0430\u0435\u0442.\u041d\u043e \u0443 Knex \u0435\u0441\u0442\u044c \u0435\u0441\u0442\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u043f\u0440\u0435\u0434\u0435\u043b. TypeScript \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u043c\u043e\u0447\u044c \u0441 \u0442\u0438\u043f\u043e\u043c \u0441\u0442\u0440\u043e\u043a\u0438 \u0447\u0435\u0440\u0435\u0437 knex&lt;User&gt;(&#8216;users&#8217;), \u043d\u043e \u0438\u043c\u0435\u043d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446 \u0438 \u043a\u043e\u043b\u043e\u043d\u043e\u043a \u0432\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u0447\u0430\u0441\u0442\u043e \u043e\u0441\u0442\u0430\u044e\u0442\u0441\u044f \u0441\u0442\u0440\u043e\u043a\u0430\u043c\u0438: &#8216;users&#8217;, &#8216;user_id&#8217;, &#8216;created_at&#8217;. \u0415\u0441\u043b\u0438 \u0432 \u043a\u043e\u0434\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0430\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u0436\u0438\u0432\u0451\u0442 \u0432 camelCase, \u0431\u0430\u0437\u0430 \u0432 snake_case, \u0430 \u0440\u044f\u0434\u043e\u043c \u0435\u0449\u0451 \u0435\u0441\u0442\u044c API-\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u044b \u0438 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f, \u043f\u043e\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0440\u0438\u0441\u043a \u0440\u0430\u0441\u0441\u0438\u043d\u0445\u0440\u043e\u043d\u0438\u0437\u0430\u0446\u0438\u0438. \u0418\u043c\u0435\u043d\u043d\u043e \u044d\u0442\u043e\u0442 \u0437\u0430\u0437\u043e\u0440 \u044f \u0438 \u0445\u043e\u0442\u0435\u043b \u0437\u0430\u043a\u0440\u044b\u0442\u044c schema-driven \u0441\u043b\u043e\u0435\u043c \u043f\u043e\u0432\u0435\u0440\u0445 Knex.\u0422\u0430\u043a \u043f\u043e\u044f\u0432\u0438\u043b\u0438\u0441\u044c \u0434\u0432\u0430 \u043f\u0430\u043a\u0435\u0442\u0430:@cleverbrush\/knex-schema \u2014 schema-aware \u0441\u043b\u043e\u0439 \u043f\u043e\u0432\u0435\u0440\u0445 Knex: \u0438\u043c\u0435\u043d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446 \u0438 \u043a\u043e\u043b\u043e\u043d\u043e\u043a, DDL-\u043c\u0435\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u0435, \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 query builder, projections, scopes, eager loading.@cleverbrush\/orm \u2014 \u0431\u043e\u043b\u0435\u0435 \u0432\u044b\u0441\u043e\u043a\u0438\u0439 \u0443\u0440\u043e\u0432\u0435\u043d\u044c: defineEntity(), DbContext, DbSet, relations, \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438, save graph \u0438 tracking context.Knex \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u043d\u0438\u043a\u0443\u0434\u0430 \u043d\u0435 \u0438\u0441\u0447\u0435\u0437\u0430\u0435\u0442. \u042d\u0442\u043e \u043d\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0430 \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c SQL \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e. \u0421\u043a\u043e\u0440\u0435\u0435 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442: Knex \u043e\u0441\u0442\u0430\u0451\u0442\u0441\u044f \u043d\u0438\u0436\u043d\u0438\u043c \u0443\u0440\u043e\u0432\u043d\u0435\u043c \u0438 escape hatch, \u0430 \u0441\u0445\u0435\u043c\u0430 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043a \u043d\u0435\u043c\u0443 \u0442\u0438\u043f\u044b \u0438 \u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0451\u043d\u043d\u043e\u0441\u0442\u0438, \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u043e\u0431\u044b\u0447\u043d\u043e \u043d\u0435 \u0445\u0432\u0430\u0442\u0430\u0435\u0442 \u0432 \u0431\u043e\u043b\u044c\u0448\u043e\u043c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438.\u0421 \u0447\u0435\u0433\u043e \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442\u0441\u044f \u0442\u0430\u0431\u043b\u0438\u0446\u0430\u0412 xpenser \u0442\u0430\u0431\u043b\u0438\u0446\u044b \u043e\u043f\u0438\u0441\u0430\u043d\u044b \u0432 apps\/api\/src\/db\/schemas.ts. \u0418\u043c\u043f\u043e\u0440\u0442 \u0438\u0434\u0451\u0442 \u0438\u0437 @cleverbrush\/orm, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e ORM \u0440\u0435\u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0438\u0440\u0443\u0435\u0442 \u0432\u0441\u0435 schema builders \u0438\u0437 @cleverbrush\/knex-schema \u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043a \u043d\u0438\u043c \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043c\u0435\u0442\u043e\u0434\u044b \u0434\u043b\u044f \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0441\u0432\u044f\u0437\u0435\u0439, DDL-\u043c\u0435\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u0445, \u043f\u0440\u043e\u0435\u043a\u0446\u0438\u0439, \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0441\u0432\u043e\u0439\u0441\u0442\u0432 \u0438 \u0442.\u0434.:import {    boolean,    type DbContext,    date,    defineEntity,    number,    object,    string} from &#8216;@cleverbrush\/orm&#8217;;\u041d\u0430\u0447\u043d\u0451\u043c \u0441 \u043f\u0440\u043e\u0441\u0442\u043e\u0439, \u043d\u043e \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0445\u0435\u043c\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f:export const UserDbSchema = object({    id: number().primaryKey(),    email: string(),    passwordHash: string().optional().hasColumnName(&#8216;password_hash&#8217;),    emailVerified: boolean().hasColumnName(&#8217;email_verified&#8217;).defaultTo(false),    role: string(),    authProvider: string().hasColumnName(&#8216;auth_provider&#8217;),    defaultCurrency: string().hasColumnName(&#8216;default_currency&#8217;),    countryCode: string().hasColumnName(&#8216;country_code&#8217;).defaultTo(&#8216;US&#8217;),    timezone: string().defaultTo(&#8216;UTC&#8217;),    createdAt: date().hasColumnName(&#8216;created_at&#8217;).defaultTo(&#8216;now&#8217;),    updatedAt: date().hasColumnName(&#8216;updated_at&#8217;).defaultTo(&#8216;now&#8217;)})    .hasTableName(&#8216;users&#8217;)    .projection(        &#8216;public&#8217;,        &#8216;id&#8217;,        &#8217;email&#8217;,        &#8217;emailVerified&#8217;,        &#8216;role&#8217;,        &#8216;authProvider&#8217;,        &#8216;defaultCurrency&#8217;,        &#8216;countryCode&#8217;,        &#8216;timezone&#8217;,        &#8216;createdAt&#8217;,        &#8216;updatedAt&#8217;    )    .projection(        &#8216;auth&#8217;,        &#8216;id&#8217;,        &#8217;email&#8217;,        &#8216;passwordHash&#8217;,        &#8217;emailVerified&#8217;,        &#8216;role&#8217;,        &#8216;authProvider&#8217;,        &#8216;defaultCurrency&#8217;,        &#8216;countryCode&#8217;,        &#8216;timezone&#8217;    );\u0417\u0434\u0435\u0441\u044c \u043e\u0431\u044b\u0447\u043d\u0430\u044f schema-\u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f \u0434\u043e\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f DB-\u043c\u0435\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u043c\u0438:.hasTableName(&#8216;users&#8217;) \u0437\u0430\u0434\u0430\u0451\u0442 \u0438\u043c\u044f \u0442\u0430\u0431\u043b\u0438\u0446\u044b..hasColumnName(&#8216;password_hash&#8217;) \u0441\u0432\u044f\u0437\u044b\u0432\u0430\u0435\u0442 camelCase-\u043f\u043e\u043b\u0435 \u0432 TypeScript \u0441\u043e snake_case-\u043a\u043e\u043b\u043e\u043d\u043a\u043e\u0439 \u0432 SQL..primaryKey() \u043f\u043e\u043c\u0435\u0447\u0430\u0435\u0442 \u043f\u0435\u0440\u0432\u0438\u0447\u043d\u044b\u0439 \u043a\u043b\u044e\u0447..defaultTo(false) \u0438 .defaultTo(&#8216;now&#8217;) \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u044e\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 DDL..projection(&#8216;auth&#8217;, &#8230;) \u0437\u0430\u0434\u0430\u0451\u0442 \u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043d\u0430\u0431\u043e\u0440 \u043a\u043e\u043b\u043e\u043d\u043e\u043a \u0434\u043b\u044f \u0432\u044b\u0431\u043e\u0440\u043a\u0438.\u0412 \u043e\u0431\u044b\u0447\u043d\u043e\u043c Knex \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c generic-\u0442\u0438\u043f\u044b \u0432 knex&lt;User&gt;(&#8216;users&#8217;), \u043d\u043e \u0438\u043c\u0435\u043d\u0430 \u043a\u043e\u043b\u043e\u043d\u043e\u043a \u0432\u0441\u0451 \u0440\u0430\u0432\u043d\u043e \u043e\u0441\u0442\u0430\u044e\u0442\u0441\u044f \u0441\u0442\u0440\u043e\u043a\u0430\u043c\u0438. \u0417\u0434\u0435\u0441\u044c TypeScript-\u0441\u0432\u043e\u0439\u0441\u0442\u0432\u043e \u0438 SQL-\u043a\u043e\u043b\u043e\u043d\u043a\u0430 \u0441\u0432\u044f\u0437\u0430\u043d\u044b \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u0432 \u0441\u0445\u0435\u043c\u0435, \u0430 \u0434\u0430\u043b\u044c\u0448\u0435 query builder \u0441\u0430\u043c \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u0438\u0442 passwordHash \u0432 password_hash.Projections: \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 SELECT\u0412 xpenser projections \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0442\u0430\u043c, \u0433\u0434\u0435 \u043d\u0435\u043b\u044c\u0437\u044f \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u043e \u0434\u043e\u0441\u0442\u0430\u0442\u044c \u043b\u0438\u0448\u043d\u0435\u0435 \u043f\u043e\u043b\u0435. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u043f\u0440\u0438 \u043b\u043e\u0433\u0438\u043d\u0435 \u043d\u0443\u0436\u0435\u043d passwordHash, \u043d\u043e \u043f\u0443\u0431\u043b\u0438\u0447\u043d\u044b\u0435 \u043e\u0442\u0432\u0435\u0442\u044b API \u043d\u0435 \u0434\u043e\u043b\u0436\u043d\u044b \u0435\u0433\u043e \u0432\u0438\u0434\u0435\u0442\u044c:const user = await db.users    .projected(&#8216;auth&#8217;)    .where(candidate =&gt; candidate.email, email)    .first();projected(&#8216;auth&#8217;) \u0434\u0435\u043b\u0430\u0435\u0442 \u0434\u0432\u0435 \u0432\u0435\u0449\u0438 \u043e\u0434\u043d\u043e\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e:\u0432 SQL \u0443\u0445\u043e\u0434\u0438\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043d\u0430\u0431\u043e\u0440 \u043a\u043e\u043b\u043e\u043d\u043e\u043a \u0438\u0437 .projection(&#8216;auth&#8217;, &#8230;);TypeScript-\u0442\u0438\u043f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0430 \u0441\u0443\u0436\u0430\u0435\u0442\u0441\u044f \u0434\u043e \u044d\u0442\u0438\u0445 \u043f\u043e\u043b\u0435\u0439.\u0415\u0441\u043b\u0438 \u0432\u044b\u0431\u0440\u0430\u0442\u044c projected(&#8216;public&#8217;), \u0442\u043e \u043e\u0431\u0440\u0430\u0442\u0438\u0442\u044c\u0441\u044f \u043a user.passwordHash \u0443\u0436\u0435 \u043d\u0435 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0441\u044f \u043d\u0430 \u044d\u0442\u0430\u043f\u0435 \u043a\u043e\u043c\u043f\u0438\u043b\u044f\u0446\u0438\u0438. \u042d\u0442\u043e \u0438\u043c\u0435\u043d\u043d\u043e \u0442\u0430 \u043c\u0435\u043b\u043e\u0447\u044c, \u0440\u0430\u0434\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0438 \u0445\u043e\u0447\u0435\u0442\u0441\u044f \u0434\u0435\u0440\u0436\u0430\u0442\u044c \u0441\u0445\u0435\u043c\u0443 \u043a\u0430\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a \u043f\u0440\u0430\u0432\u0434\u044b, \u0430 \u043d\u0435 \u043a\u0430\u043a \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u0441 \u0442\u0438\u043f\u0430\u043c\u0438 \u0440\u044f\u0434\u043e\u043c \u0441 SQL.Foreign keys \u0438 \u0438\u043d\u0434\u0435\u043a\u0441\u044b\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u043e\u0441\u043c\u043e\u0442\u0440\u0438\u043c \u043d\u0430 \u0434\u043e\u043c\u0435\u043d\u043d\u0443\u044e \u0447\u0430\u0441\u0442\u044c xpenser. \u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0441\u043e\u0437\u0434\u0430\u0451\u0442 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u0438, \u043f\u0440\u043e\u0434\u0430\u0432\u0446\u043e\u0432 \u0438 \u0442\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u0438. \u0422\u0440\u0430\u043d\u0437\u0430\u043a\u0446\u0438\u044f \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0438\u0442 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044e, \u0438\u043c\u0435\u0435\u0442 \u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0438\u044e, \u043e\u043f\u0446\u0438\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0434\u0430\u0432\u0446\u0430, \u0432\u0430\u043b\u044e\u0442\u0443, \u0441\u0443\u043c\u043c\u0443, \u0434\u0430\u0442\u0443 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u0438 \u0438 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u0430\u043d\u043d\u0443\u044e \u0441\u0443\u043c\u043c\u0443 \u0432 \u0432\u0430\u043b\u044e\u0442\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f:export const TransactionDbSchema = object({    id: number().primaryKey(),    userId: number()        .hasColumnName(&#8216;user_id&#8217;)        .references(&#8216;users&#8217;, &#8216;id&#8217;)        .onDelete(&#8216;CASCADE&#8217;)        .index(&#8216;idx_transactions_user_id&#8217;),    categoryId: number()        .hasColumnName(&#8216;category_id&#8217;)        .references(&#8216;categories&#8217;, &#8216;id&#8217;)        .onDelete(&#8216;RESTRICT&#8217;)        .index(&#8216;idx_transactions_category_id&#8217;),    vendorId: number()        .hasColumnName(&#8216;vendor_id&#8217;)        .references(&#8216;vendors&#8217;, &#8216;id&#8217;)        .onDelete(&#8216;SET NULL&#8217;)        .index(&#8216;idx_transactions_vendor_id&#8217;)        .optional(),    type: string(),    amount: number(),    currency: string(),    defaultCurrencyAmount: number().hasColumnName(&#8216;default_currency_amount&#8217;),    defaultCurrency: string().hasColumnName(&#8216;default_currency&#8217;),    exchangeRate: number().hasColumnName(&#8216;exchange_rate&#8217;),    exchangeRateDate: string().hasColumnName(&#8216;exchange_rate_date&#8217;),    occurredAt: date().hasColumnName(&#8216;occurred_at&#8217;),    note: string().optional(),    createdAt: date().hasColumnName(&#8216;created_at&#8217;).defaultTo(&#8216;now&#8217;),    updatedAt: date().hasColumnName(&#8216;updated_at&#8217;).defaultTo(&#8216;now&#8217;),    category: CategoryDbSchema.optional()}).hasTableName(&#8216;transactions&#8217;);\u041c\u0435\u0442\u043e\u0434\u044b .references(), .onDelete() \u0438 .index() \u043d\u0443\u0436\u043d\u044b \u043d\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0447\u0438\u0442\u0430\u0431\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438. \u041e\u043d\u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u044e\u0442\u0441\u044f \u0432 introspection metadata \u0441\u0445\u0435\u043c\u044b, \u0430 \u0437\u043d\u0430\u0447\u0438\u0442 \u0438\u0445 \u043c\u043e\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043b\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 DDL, \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u044f \u0441\u0445\u0435\u043c \u0438 \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u044f \u043c\u0438\u0433\u0440\u0430\u0446\u0438\u0439.\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435 \u043d\u0430 category: CategoryDbSchema.optional(). \u042d\u0442\u043e \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u043e: \u043e\u043d\u043e \u043d\u0443\u0436\u043d\u043e ORM, \u0447\u0442\u043e\u0431\u044b \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442 .include(&#8230;) \u0431\u044b\u043b \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c. \u0421\u0430\u043c\u0430 \u0441\u0432\u044f\u0437\u044c \u043e\u0431\u044a\u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e, \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 entity.Entity \u0438 relations\u0421\u0445\u0435\u043c\u0430 \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u0442 \u0444\u043e\u0440\u043c\u0443 \u0441\u0442\u0440\u043e\u043a\u0438 \u0438 DB-\u043c\u0435\u0442\u0430\u0434\u0430\u043d\u043d\u044b\u0435. Entity \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u0441\u0432\u044f\u0437\u0438:export const CategoryEntity = defineEntity(CategoryDbSchema);export const VendorEntity = defineEntity(VendorDbSchema);export const TransactionEntity = defineEntity(TransactionDbSchema).belongsTo(    t =&gt; t.category,    l =&gt; l.categoryId,    r =&gt; r.id);\u0412 \u044d\u0442\u043e\u043c \u043f\u0440\u0438\u043c\u0435\u0440\u0435:t =&gt; t.category \u2014 \u043d\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0435;l =&gt; l.categoryId \u2014 FK \u043d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0435 transactions;r =&gt; r.id \u2014 PK \u043d\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u0435 categories.\u0412\u0441\u0435 \u0442\u0440\u0438 \u0432\u044b\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u044b. \u0415\u0441\u043b\u0438 \u043f\u0435\u0440\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u0442\u044c categoryId \u0438\u043b\u0438 \u043f\u043e\u043f\u044b\u0442\u0430\u0442\u044c\u0441\u044f \u0441\u0432\u044f\u0437\u0430\u0442\u044c \u0447\u0438\u0441\u043b\u043e \u0441\u043e \u0441\u0442\u0440\u043e\u043a\u043e\u0439, TypeScript \u043f\u043e\u043a\u0430\u0436\u0435\u0442 \u043e\u0448\u0438\u0431\u043a\u0443 \u0434\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.\u0414\u043b\u044f \u0434\u0440\u0443\u0433\u0438\u0445 \u0441\u043b\u0443\u0447\u0430\u0435\u0432 \u0435\u0441\u0442\u044c .hasOne(), .hasMany() \u0438 .belongsToMany(). \u0412 xpenser \u043f\u043e\u043a\u0430 \u0445\u0432\u0430\u0442\u0430\u0435\u0442 belongsTo, \u043d\u043e \u0432 \u0442\u0435\u0441\u0442\u0430\u0445 ORM \u043f\u043e\u043a\u0440\u044b\u0442\u044b \u0438 \u0431\u043e\u043b\u0435\u0435 \u0441\u043b\u043e\u0436\u043d\u044b\u0435 \u0433\u0440\u0430\u0444\u044b: \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0435 \u0434\u0435\u0440\u0435\u0432\u0430 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432, many-to-many \u0447\u0435\u0440\u0435\u0437 pivot \u0442\u0430\u0431\u043b\u0438\u0446\u0443, composite primary keys \u0438 polymorphic variants.DbContext: \u043a\u0430\u0440\u0442\u0430 \u0432\u0441\u0435\u0445 \u0442\u0430\u0431\u043b\u0438\u0446\u041f\u043e\u0441\u043b\u0435 \u043e\u0431\u044a\u044f\u0432\u043b\u0435\u043d\u0438\u044f entity \u0441\u043e\u0431\u0438\u0440\u0430\u044e\u0442\u0441\u044f \u0432 \u043e\u0431\u044b\u0447\u043d\u044b\u0439 \u043e\u0431\u044a\u0435\u043a\u0442:export const entityMap = {    users: UserEntity,    categories: CategoryEntity,    vendors: VendorEntity,    transactions: TransactionEntity,    transactionScans: TransactionScanEntity,    transactionScanItems: TransactionScanItemEntity,    transactionScanImages: TransactionScanImageEntity,    exchangeRates: ExchangeRateEntity};export type AppEntityMap = typeof entityMap;export type AppDb = DbContext&lt;AppEntityMap&gt;;\u041d\u0430 \u0441\u0442\u0430\u0440\u0442\u0435 API \u0441\u043e\u0437\u0434\u0430\u0451\u0442\u0441\u044f DbContext:import { createDb } from &#8216;@cleverbrush\/orm&#8217;;import { instrumentKnex } from &#8216;@cleverbrush\/otel&#8217;;import knex from &#8216;knex&#8217;;import { entityMap } from &#8216;..\/db\/schemas.js&#8217;;const connection = instrumentKnex(    knex({        client: &#8216;pg&#8217;,        connection: config.db.connectionString,        pool: { min: 2, max: 10 },        acquireConnectionTimeout: 10_000    }),    { sanitizeStatement: () =&gt; &#8216;&lt;redacted&gt;&#8217; });const db = createDb(connection, entityMap);\u041d\u0430 \u0432\u044b\u0445\u043e\u0434\u0435 db.users, db.categories, db.transactions \u0438 \u043e\u0441\u0442\u0430\u043b\u044c\u043d\u044b\u0435 \u043f\u043e\u043b\u044f \u0441\u0442\u0430\u043d\u043e\u0432\u044f\u0442\u0441\u044f \u0442\u0438\u043f\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 DbSet. \u042d\u0442\u043e \u043f\u043e\u0445\u043e\u0436\u0435 \u043d\u0430 DbContext \u0438\u0437 Entity Framework, \u0442\u043e\u043b\u044c\u043a\u043e \u0431\u0435\u0437 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440\u043e\u0432, reflection metadata \u0438 \u043a\u043e\u0434\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438.\u0417\u0430\u043f\u0440\u043e\u0441\u044b: \u043e\u0442 \u043f\u0440\u043e\u0441\u0442\u043e\u0433\u043e \u043a \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u043c\u0443\u0421\u0430\u043c\u044b\u0439 \u043f\u0440\u043e\u0441\u0442\u043e\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u0442\u0430\u043a:const categories = await &#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-483290","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/483290","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=483290"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/483290\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=483290"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=483290"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=483290"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}