sonamu 0.9.13 → 0.9.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/database/puri.d.ts +13 -1
- package/dist/database/puri.d.ts.map +1 -1
- package/dist/database/puri.js +47 -5
- package/dist/entity/entity-manager.d.ts +1 -0
- package/dist/entity/entity-manager.d.ts.map +1 -1
- package/dist/migration/code-generation.d.ts.map +1 -1
- package/dist/migration/code-generation.js +74 -8
- package/dist/migration/postgresql-schema-reader.d.ts +1 -0
- package/dist/migration/postgresql-schema-reader.d.ts.map +1 -1
- package/dist/migration/postgresql-schema-reader.js +4 -2
- package/dist/template/implementations/sd.template.d.ts.map +1 -1
- package/dist/template/implementations/sd.template.js +70 -14
- package/dist/types/types.d.ts +6 -0
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/types.js +2 -1
- package/dist/utils/utils.d.ts +1 -1
- package/dist/utils/utils.d.ts.map +1 -1
- package/dist/utils/utils.js +1 -1
- package/package.json +2 -2
- package/src/database/puri.ts +92 -7
- package/src/database/puri.types.test-d.ts +70 -0
- package/src/migration/__tests__/code-generation.search-text.test.ts +146 -1
- package/src/migration/code-generation.ts +85 -6
- package/src/migration/postgresql-schema-reader.ts +4 -1
- package/src/skills/sonamu/i18n.md +9 -1
- package/src/template/implementations/sd.template.ts +69 -13
- package/src/types/__tests__/entity-json-schema-search-text.test.ts +39 -0
- package/src/types/types.ts +5 -0
- package/src/ui/entity.instructions.md +7 -3
- package/src/utils/utils.ts +1 -1
package/dist/utils/utils.js
CHANGED
|
@@ -93,4 +93,4 @@ var init_utils = __esmMin((() => {}));
|
|
|
93
93
|
//#endregion
|
|
94
94
|
init_utils();
|
|
95
95
|
export { assertDefined, assertExists, assertNotNull, convertFastifyHeadersToStandard, differenceWith, exhaustive, findApiRootPath, findAppRootPath, init_utils, intersectionBy, isPlainObject, merge, nonNullable };
|
|
96
|
-
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL3V0aWxzLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBmcyBmcm9tIFwiZnNcIjtcbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5cbmltcG9ydCB7IHR5cGUgRmFzdGlmeVJlcXVlc3QgfSBmcm9tIFwiZmFzdGlmeVwiO1xuXG5pbXBvcnQgeyB0eXBlIEFic29sdXRlUGF0aCB9IGZyb20gXCIuL3BhdGgtdXRpbHNcIjtcblxuZXhwb3J0IGZ1bmN0aW9uIGZpbmRBcHBSb290UGF0aCgpOiBBYnNvbHV0ZVBhdGgge1xuICBjb25zdCBhcGlSb290UGF0aCA9IGZpbmRBcGlSb290UGF0aCgpO1xuICByZXR1cm4gcGF0aC5kaXJuYW1lKGFwaVJvb3RQYXRoKSBhcyBBYnNvbHV0ZVBhdGg7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBmaW5kQXBpUm9vdFBhdGgoKTogQWJzb2x1dGVQYXRoIHtcbiAgLy8gTk9URTogZm9yIHN1cHBvcnQgbnBtIC8geWFybiAvIHBucG0gd29ya3NwYWNlc1xuICAvLyDtlZjsp4Drp4wgd29ya3NwYWNlIOyTsOuptCBwcm9jZXNzLmN3ZCgpIO2VmOuptCDrkJjripTrjbAuLi4g7J206rG0IOuCmOykkeyXkCDtmJHsnZgg7ZuEIOyImOygle2VmOuKlOqxuOuhnFxuICBjb25zdCB3b3Jrc3BhY2VQYXRoID0gcHJvY2Vzcy5lbnYuUE5QTV9TQ1JJUFRfU1JDX0RJUiA/PyBwcm9jZXNzLmVudi5JTklUX0NXRDtcbiAgaWYgKG5vbk51bGxhYmxlKHdvcmtzcGFjZVBhdGgpKSB7XG4gICAgcmV0dXJuIHdvcmtzcGFjZVBhdGggYXMgQWJzb2x1dGVQYXRoO1xuICB9XG5cbiAgaWYgKG5vbk51bGxhYmxlKHByb2Nlc3MuZW52LlBOUE1fUEFDS0FHRV9OQU1FKSkge1xuICAgIHJldHVybiBwcm9jZXNzLmN3ZCgpLnNwbGl0KHBhdGguc2VwKS5qb2luKHBhdGguc2VwKSBhcyBBYnNvbHV0ZVBhdGg7XG4gIH1cblxuICBjb25zdCBjd2RQYWNrYWdlUGF0aCA9IHBhdGguam9pbihwcm9jZXNzLmN3ZCgpLCBcInBhY2thZ2UuanNvblwiKTtcbiAgaWYgKGZzLmV4aXN0c1N5bmMoY3dkUGFja2FnZVBhdGgpKSB7XG4gICAgcmV0dXJuIHByb2Nlc3MuY3dkKCkuc3BsaXQocGF0aC5zZXApLmpvaW4ocGF0aC5zZXApIGFzIEFic29sdXRlUGF0aDtcbiAgfVxuXG4gIGNvbnN0IGJhc2VQYXRoID0gaW1wb3J0Lm1ldGEuZmlsZW5hbWU7XG4gIGxldCBkaXIgPSBwYXRoLmRpcm5hbWUoYmFzZVBhdGgpO1xuICBpZiAoZGlyLmluY2x1ZGVzKFwiLy55YXJuL1wiKSkge1xuICAgIGRpciA9IGRpci5zcGxpdChcIi8ueWFybi9cIilbMF07XG4gIH1cblxuICBkbyB7XG4gICAgaWYgKGZzLmV4aXN0c1N5bmMocGF0aC5qb2luKGRpciwgXCIvcGFja2FnZS5qc29uXCIpKSkge1xuICAgICAgcmV0dXJuIGRpci5zcGxpdChwYXRoLnNlcCkuam9pbihwYXRoLnNlcCkgYXMgQWJzb2x1dGVQYXRoO1xuICAgIH1cbiAgICBkaXIgPSBkaXIuc3BsaXQocGF0aC5zZXApLnNsaWNlKDAsIC0xKS5qb2luKHBhdGguc2VwKTtcbiAgfSB3aGlsZSAoZGlyLnNwbGl0KHBhdGguc2VwKS5sZW5ndGggPiAxKTtcbiAgdGhyb3cgbmV3IEVycm9yKFwiQ2Fubm90IGZpbmQgQXBwUm9vdCB1c2luZyBTb25hbXUgLTJcIik7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBub25OdWxsYWJsZTxUPih2YWx1ZTogVCk6IHZhbHVlIGlzIE5vbk51bGxhYmxlPFQ+
|
|
96
|
+
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXRpbHMuanMiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiLi4vLi4vc3JjL3V0aWxzL3V0aWxzLnRzIl0sInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBmcyBmcm9tIFwiZnNcIjtcbmltcG9ydCBwYXRoIGZyb20gXCJwYXRoXCI7XG5cbmltcG9ydCB7IHR5cGUgRmFzdGlmeVJlcXVlc3QgfSBmcm9tIFwiZmFzdGlmeVwiO1xuXG5pbXBvcnQgeyB0eXBlIEFic29sdXRlUGF0aCB9IGZyb20gXCIuL3BhdGgtdXRpbHNcIjtcblxuZXhwb3J0IGZ1bmN0aW9uIGZpbmRBcHBSb290UGF0aCgpOiBBYnNvbHV0ZVBhdGgge1xuICBjb25zdCBhcGlSb290UGF0aCA9IGZpbmRBcGlSb290UGF0aCgpO1xuICByZXR1cm4gcGF0aC5kaXJuYW1lKGFwaVJvb3RQYXRoKSBhcyBBYnNvbHV0ZVBhdGg7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBmaW5kQXBpUm9vdFBhdGgoKTogQWJzb2x1dGVQYXRoIHtcbiAgLy8gTk9URTogZm9yIHN1cHBvcnQgbnBtIC8geWFybiAvIHBucG0gd29ya3NwYWNlc1xuICAvLyDtlZjsp4Drp4wgd29ya3NwYWNlIOyTsOuptCBwcm9jZXNzLmN3ZCgpIO2VmOuptCDrkJjripTrjbAuLi4g7J206rG0IOuCmOykkeyXkCDtmJHsnZgg7ZuEIOyImOygle2VmOuKlOqxuOuhnFxuICBjb25zdCB3b3Jrc3BhY2VQYXRoID0gcHJvY2Vzcy5lbnYuUE5QTV9TQ1JJUFRfU1JDX0RJUiA/PyBwcm9jZXNzLmVudi5JTklUX0NXRDtcbiAgaWYgKG5vbk51bGxhYmxlKHdvcmtzcGFjZVBhdGgpKSB7XG4gICAgcmV0dXJuIHdvcmtzcGFjZVBhdGggYXMgQWJzb2x1dGVQYXRoO1xuICB9XG5cbiAgaWYgKG5vbk51bGxhYmxlKHByb2Nlc3MuZW52LlBOUE1fUEFDS0FHRV9OQU1FKSkge1xuICAgIHJldHVybiBwcm9jZXNzLmN3ZCgpLnNwbGl0KHBhdGguc2VwKS5qb2luKHBhdGguc2VwKSBhcyBBYnNvbHV0ZVBhdGg7XG4gIH1cblxuICBjb25zdCBjd2RQYWNrYWdlUGF0aCA9IHBhdGguam9pbihwcm9jZXNzLmN3ZCgpLCBcInBhY2thZ2UuanNvblwiKTtcbiAgaWYgKGZzLmV4aXN0c1N5bmMoY3dkUGFja2FnZVBhdGgpKSB7XG4gICAgcmV0dXJuIHByb2Nlc3MuY3dkKCkuc3BsaXQocGF0aC5zZXApLmpvaW4ocGF0aC5zZXApIGFzIEFic29sdXRlUGF0aDtcbiAgfVxuXG4gIGNvbnN0IGJhc2VQYXRoID0gaW1wb3J0Lm1ldGEuZmlsZW5hbWU7XG4gIGxldCBkaXIgPSBwYXRoLmRpcm5hbWUoYmFzZVBhdGgpO1xuICBpZiAoZGlyLmluY2x1ZGVzKFwiLy55YXJuL1wiKSkge1xuICAgIGRpciA9IGRpci5zcGxpdChcIi8ueWFybi9cIilbMF07XG4gIH1cblxuICBkbyB7XG4gICAgaWYgKGZzLmV4aXN0c1N5bmMocGF0aC5qb2luKGRpciwgXCIvcGFja2FnZS5qc29uXCIpKSkge1xuICAgICAgcmV0dXJuIGRpci5zcGxpdChwYXRoLnNlcCkuam9pbihwYXRoLnNlcCkgYXMgQWJzb2x1dGVQYXRoO1xuICAgIH1cbiAgICBkaXIgPSBkaXIuc3BsaXQocGF0aC5zZXApLnNsaWNlKDAsIC0xKS5qb2luKHBhdGguc2VwKTtcbiAgfSB3aGlsZSAoZGlyLnNwbGl0KHBhdGguc2VwKS5sZW5ndGggPiAxKTtcbiAgdGhyb3cgbmV3IEVycm9yKFwiQ2Fubm90IGZpbmQgQXBwUm9vdCB1c2luZyBTb25hbXUgLTJcIik7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBub25OdWxsYWJsZTxUPih2YWx1ZTogVCk6IHZhbHVlIGlzIE5vbk51bGxhYmxlPFQ+IHtcbiAgcmV0dXJuIHZhbHVlICE9PSBudWxsICYmIHZhbHVlICE9PSB1bmRlZmluZWQ7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBleGhhdXN0aXZlKF9wYXJhbTogbmV2ZXIpOiBuZXZlciB7XG4gIHRocm93IG5ldyBFcnJvcihgZXhoYXVzdGl2ZWApO1xufVxuXG4vLyDsnbzrsJgg67KE7KCEXG5leHBvcnQgZnVuY3Rpb24gYXNzZXJ0RXhpc3RzPFQ+KHZhbHVlOiBUIHwgbnVsbCB8IHVuZGVmaW5lZCwgbWVzc2FnZT86IHN0cmluZyk6IFQge1xuICBpZiAodmFsdWUgPT09IG51bGwgfHwgdmFsdWUgPT09IHVuZGVmaW5lZCkge1xuICAgIHRocm93IG5ldyBFcnJvcihtZXNzYWdlID8/IFwiVmFsdWUgbXVzdCBleGlzdFwiKTtcbiAgfVxuICByZXR1cm4gdmFsdWU7XG59XG5cbi8vIG51bGzrp4wg7LK07YGsXG5leHBvcnQgZnVuY3Rpb24gYXNzZXJ0Tm90TnVsbDxUPih2YWx1ZTogVCB8IG51bGwsIG1lc3NhZ2U/OiBzdHJpbmcpOiBUIHtcbiAgaWYgKHZhbHVlID09PSBudWxsKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKG1lc3NhZ2UgPz8gXCJWYWx1ZSBtdXN0IG5vdCBiZSBudWxsXCIpO1xuICB9XG4gIHJldHVybiB2YWx1ZTtcbn1cbi8vIHVuZGVmaW5lZOunjCDssrTtgaxcbmV4cG9ydCBmdW5jdGlvbiBhc3NlcnREZWZpbmVkPFQ+KHZhbHVlOiBUIHwgdW5kZWZpbmVkLCBtZXNzYWdlPzogc3RyaW5nKTogVCB7XG4gIGlmICh2YWx1ZSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKG1lc3NhZ2UgPz8gXCJWYWx1ZSBtdXN0IGJlIGRlZmluZWRcIik7XG4gIH1cbiAgcmV0dXJuIHZhbHVlO1xufVxuXG4vLyBsb2Rhc2ggaW50ZXJzZWN0aW9uQnkg64yA7LK0XG5leHBvcnQgZnVuY3Rpb24gaW50ZXJzZWN0aW9uQnk8VCwgSz4oXG4gIGFycjE6IHJlYWRvbmx5IFRbXSxcbiAgYXJyMjogcmVhZG9ubHkgVFtdLFxuICBpdGVyYXRlZTogKGl0ZW06IFQpID0+IEssXG4pOiBUW10ge1xuICBjb25zdCBhcnIyS2V5cyA9IG5ldyBTZXQoYXJyMi5tYXAoaXRlcmF0ZWUpKTtcbiAgcmV0dXJuIGFycjEuZmlsdGVyKChpdGVtKSA9PiBhcnIyS2V5cy5oYXMoaXRlcmF0ZWUoaXRlbSkpKTtcbn1cbi8vIGxvZGFzaCBkaWZmZXJlbmNlV2l0aCDrjIDssrRcbmV4cG9ydCBmdW5jdGlvbiBkaWZmZXJlbmNlV2l0aDxUPihcbiAgYXJyMTogcmVhZG9ubHkgVFtdLFxuICBhcnIyOiByZWFkb25seSBUW10sXG4gIGNvbXBhcmF0b3I6IChhOiBULCBiOiBUKSA9PiBib29sZWFuLFxuKTogVFtdIHtcbiAgcmV0dXJuIGFycjEuZmlsdGVyKChpdGVtQSkgPT4gIWFycjIuc29tZSgoaXRlbUIpID0+IGNvbXBhcmF0b3IoaXRlbUEsIGl0ZW1CKSkpO1xufVxuXG4vLyBiaW9tZS1pZ25vcmUgbGludC9zdXNwaWNpb3VzL25vRXhwbGljaXRBbnk6IGR5bmFtaWMgcHJvcGVydHkgYWNjZXNzXG5leHBvcnQgZnVuY3Rpb24gbWVyZ2U8VCBleHRlbmRzIFJlY29yZDxzdHJpbmcsIGFueT4+KGRlZmF1bHRPYmo6IFQsIHVzZXJPYmo6IFQpOiBUIHtcbiAgLy8g7JuQ67O4IOuztOyhtOydhCDsnITtlbQgZGVmYXVsdE9iaiDrs7XsgqxcbiAgY29uc3QgcmVzdWx0ID0geyAuLi5kZWZhdWx0T2JqIH07XG5cbiAgLy8gdXNlck9iauydmCDqsIEg7IaN7ISx7J2EIOyInO2ajFxuICBmb3IgKGNvbnN0IGtleSBpbiB1c2VyT2JqKSB7XG4gICAgLy8gdXNlck9iauydmCBvd24gcHJvcGVydHnrp4wg7LKY66asICjtlITroZzthqDtg4DsnoUg7LK07J24IOygnOyZuClcbiAgICBpZiAoT2JqZWN0Lmhhc093bih1c2VyT2JqLCBrZXkpKSB7XG4gICAgICBjb25zdCB1c2VyVmFsdWUgPSB1c2VyT2JqW2tleV07XG4gICAgICBjb25zdCBkZWZhdWx0VmFsdWUgPSByZXN1bHRba2V5XTtcblxuICAgICAgLy8g65GQIOqwkuydtCDrqqjrkZAg6rCd7LK07J206rOgLCDrsLDsl7TsnbQg7JWE64uMIOqyveyasCDsnqzqt4DsoIHsnLzroZwg67OR7ZWpXG4gICAgICBpZiAoaXNQbGFpbk9iamVjdCh1c2VyVmFsdWUpICYmIGlzUGxhaW5PYmplY3QoZGVmYXVsdFZhbHVlKSkge1xuICAgICAgICByZXN1bHRba2V5XSA9IG1lcmdlKGRlZmF1bHRWYWx1ZSwgdXNlclZhbHVlKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIC8vIOq3uCDsmbjsnZgg6rK97JqwIHVzZXJPYmrsnZgg6rCS7Jy866GcIOuNruyWtOyTsOq4sFxuICAgICAgICByZXN1bHRba2V5XSA9IHVzZXJWYWx1ZTtcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICByZXR1cm4gcmVzdWx0O1xufVxuXG4vLyBwbGFpbiBvYmplY3Qg7YyQ67OEIO2XrO2NvCDtlajsiJhcbi8vICjrsLDsl7QsIG51bGwsIERhdGUg65Ox7J2EIOygnOyZuO2VnCDsiJzsiJgg6rCd7LK066eMIHRydWUpXG5leHBvcnQgZnVuY3Rpb24gaXNQbGFpbk9iamVjdCh2YWx1ZTogdW5rbm93bik6IHZhbHVlIGlzIFJlY29yZDxzdHJpbmcsIHVua25vd24+IHtcbiAgcmV0dXJuIChcbiAgICB2YWx1ZSAhPT0gbnVsbCAmJlxuICAgIHR5cGVvZiB2YWx1ZSA9PT0gXCJvYmplY3RcIiAmJlxuICAgICFBcnJheS5pc0FycmF5KHZhbHVlKSAmJlxuICAgIE9iamVjdC5wcm90b3R5cGUudG9TdHJpbmcuY2FsbCh2YWx1ZSkgPT09IFwiW29iamVjdCBPYmplY3RdXCJcbiAgKTtcbn1cblxuLy8gQ29udmVydCBGYXN0aWZ5IGhlYWRlcnMgdG8gc3RhbmRhcmQgSGVhZGVycyBvYmplY3RcbmV4cG9ydCBmdW5jdGlvbiBjb252ZXJ0RmFzdGlmeUhlYWRlcnNUb1N0YW5kYXJkKGhlYWRlcnM6IEZhc3RpZnlSZXF1ZXN0W1wiaGVhZGVyc1wiXSk6IEhlYWRlcnMge1xuICBjb25zdCBoZWFkZXJzT2JqID0gbmV3IEhlYWRlcnMoKTtcbiAgT2JqZWN0LmVudHJpZXMoaGVhZGVycykuZm9yRWFjaCgoW2tleSwgdmFsdWVdKSA9PiB7XG4gICAgaWYgKHZhbHVlKSBoZWFkZXJzT2JqLmFwcGVuZChrZXksIHZhbHVlLnRvU3RyaW5nKCkpO1xuICB9KTtcbiAgcmV0dXJuIGhlYWRlcnNPYmo7XG59XG4iXSwibWFwcGluZ3MiOiI7Ozs7O0FBT0EsU0FBZ0Isa0JBQWdDO0NBQzlDLE1BQU0sY0FBYyxpQkFBaUI7QUFDckMsUUFBTyxLQUFLLFFBQVEsWUFBWTs7QUFHbEMsU0FBZ0Isa0JBQWdDO0NBRzlDLE1BQU0sZ0JBQWdCLFFBQVEsSUFBSSx1QkFBdUIsUUFBUSxJQUFJO0FBQ3JFLEtBQUksWUFBWSxjQUFjLEVBQUU7QUFDOUIsU0FBTzs7QUFHVCxLQUFJLFlBQVksUUFBUSxJQUFJLGtCQUFrQixFQUFFO0FBQzlDLFNBQU8sUUFBUSxLQUFLLENBQUMsTUFBTSxLQUFLLElBQUksQ0FBQyxLQUFLLEtBQUssSUFBSTs7Q0FHckQsTUFBTSxpQkFBaUIsS0FBSyxLQUFLLFFBQVEsS0FBSyxFQUFFLGVBQWU7QUFDL0QsS0FBSSxHQUFHLFdBQVcsZUFBZSxFQUFFO0FBQ2pDLFNBQU8sUUFBUSxLQUFLLENBQUMsTUFBTSxLQUFLLElBQUksQ0FBQyxLQUFLLEtBQUssSUFBSTs7Q0FHckQsTUFBTSxXQUFXLE9BQU8sS0FBSztDQUM3QixJQUFJLE1BQU0sS0FBSyxRQUFRLFNBQVM7QUFDaEMsS0FBSSxJQUFJLFNBQVMsVUFBVSxFQUFFO0FBQzNCLFFBQU0sSUFBSSxNQUFNLFVBQVUsQ0FBQzs7QUFHN0IsSUFBRztBQUNELE1BQUksR0FBRyxXQUFXLEtBQUssS0FBSyxLQUFLLGdCQUFnQixDQUFDLEVBQUU7QUFDbEQsVUFBTyxJQUFJLE1BQU0sS0FBSyxJQUFJLENBQUMsS0FBSyxLQUFLLElBQUk7O0FBRTNDLFFBQU0sSUFBSSxNQUFNLEtBQUssSUFBSSxDQUFDLE1BQU0sR0FBRyxDQUFDLEVBQUUsQ0FBQyxLQUFLLEtBQUssSUFBSTtVQUM5QyxJQUFJLE1BQU0sS0FBSyxJQUFJLENBQUMsU0FBUztBQUN0QyxPQUFNLElBQUksTUFBTSxzQ0FBc0M7O0FBR3hELFNBQWdCLFlBQWUsT0FBbUM7QUFDaEUsUUFBTyxVQUFVLFFBQVEsVUFBVTs7QUFHckMsU0FBZ0IsV0FBVyxRQUFzQjtBQUMvQyxPQUFNLElBQUksTUFBTSxhQUFhOztBQUkvQixTQUFnQixhQUFnQixPQUE2QixTQUFxQjtBQUNoRixLQUFJLFVBQVUsUUFBUSxVQUFVLFdBQVc7QUFDekMsUUFBTSxJQUFJLE1BQU0sV0FBVyxtQkFBbUI7O0FBRWhELFFBQU87O0FBSVQsU0FBZ0IsY0FBaUIsT0FBaUIsU0FBcUI7QUFDckUsS0FBSSxVQUFVLE1BQU07QUFDbEIsUUFBTSxJQUFJLE1BQU0sV0FBVyx5QkFBeUI7O0FBRXRELFFBQU87O0FBR1QsU0FBZ0IsY0FBaUIsT0FBc0IsU0FBcUI7QUFDMUUsS0FBSSxVQUFVLFdBQVc7QUFDdkIsUUFBTSxJQUFJLE1BQU0sV0FBVyx3QkFBd0I7O0FBRXJELFFBQU87O0FBSVQsU0FBZ0IsZUFDZCxNQUNBLE1BQ0EsVUFDSztDQUNMLE1BQU0sV0FBVyxJQUFJLElBQUksS0FBSyxJQUFJLFNBQVMsQ0FBQztBQUM1QyxRQUFPLEtBQUssUUFBUSxTQUFTLFNBQVMsSUFBSSxTQUFTLEtBQUssQ0FBQyxDQUFDOztBQUc1RCxTQUFnQixlQUNkLE1BQ0EsTUFDQSxZQUNLO0FBQ0wsUUFBTyxLQUFLLFFBQVEsVUFBVSxDQUFDLEtBQUssTUFBTSxVQUFVLFdBQVcsT0FBTyxNQUFNLENBQUMsQ0FBQzs7QUFJaEYsU0FBZ0IsTUFBcUMsWUFBZSxTQUFlO0NBRWpGLE1BQU0sU0FBUyxFQUFFLEdBQUcsWUFBWTtBQUdoQyxNQUFLLE1BQU0sT0FBTyxTQUFTO0FBRXpCLE1BQUksT0FBTyxPQUFPLFNBQVMsSUFBSSxFQUFFO0dBQy9CLE1BQU0sWUFBWSxRQUFRO0dBQzFCLE1BQU0sZUFBZSxPQUFPO0FBRzVCLE9BQUksY0FBYyxVQUFVLElBQUksY0FBYyxhQUFhLEVBQUU7QUFDM0QsV0FBTyxPQUFPLE1BQU0sY0FBYyxVQUFVO1VBQ3ZDO0FBRUwsV0FBTyxPQUFPOzs7O0FBS3BCLFFBQU87O0FBS1QsU0FBZ0IsY0FBYyxPQUFrRDtBQUM5RSxRQUNFLFVBQVUsUUFDVixPQUFPLFVBQVUsWUFDakIsQ0FBQyxNQUFNLFFBQVEsTUFBTSxJQUNyQixPQUFPLFVBQVUsU0FBUyxLQUFLLE1BQU0sS0FBSzs7QUFLOUMsU0FBZ0IsZ0NBQWdDLFNBQTZDO0NBQzNGLE1BQU0sYUFBYSxJQUFJLFNBQVM7QUFDaEMsUUFBTyxRQUFRLFFBQVEsQ0FBQyxTQUFTLENBQUMsS0FBSyxXQUFXO0FBQ2hELE1BQUksTUFBTyxZQUFXLE9BQU8sS0FBSyxNQUFNLFVBQVUsQ0FBQztHQUNuRDtBQUNGLFFBQU8ifQ==
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sonamu",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.15",
|
|
4
4
|
"description": "Sonamu — TypeScript Fullstack API Framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -129,8 +129,8 @@
|
|
|
129
129
|
"tsicli": "^1.0.5",
|
|
130
130
|
"vite": "8.0.5",
|
|
131
131
|
"vitest": "^4.1.2",
|
|
132
|
-
"@sonamu-kit/ts-loader": "^2.2.0",
|
|
133
132
|
"@sonamu-kit/hmr-runner": "^0.2.0",
|
|
133
|
+
"@sonamu-kit/ts-loader": "^2.2.0",
|
|
134
134
|
"@sonamu-kit/hmr-hook": "^0.5.1",
|
|
135
135
|
"@sonamu-kit/tasks": "^0.3.0"
|
|
136
136
|
},
|
package/src/database/puri.ts
CHANGED
|
@@ -40,6 +40,25 @@ import {
|
|
|
40
40
|
} from "./puri.types";
|
|
41
41
|
import { FUZZY_OPERATORS } from "./puri.types";
|
|
42
42
|
|
|
43
|
+
type PuriOrderByDirection = "asc" | "desc";
|
|
44
|
+
type PuriOrderByNulls = "first" | "last";
|
|
45
|
+
type PuriOrderByExpression = SqlExpression<"number"> | SqlExpression<"string">;
|
|
46
|
+
type PuriOrderByItem<TColumn extends string> = {
|
|
47
|
+
column: TColumn | PuriOrderByExpression;
|
|
48
|
+
order?: PuriOrderByDirection;
|
|
49
|
+
nulls?: PuriOrderByNulls;
|
|
50
|
+
};
|
|
51
|
+
type PuriOrderByEntry<TColumn extends string> =
|
|
52
|
+
| TColumn
|
|
53
|
+
| PuriOrderByExpression
|
|
54
|
+
| PuriOrderByItem<TColumn>;
|
|
55
|
+
type PuriOrderByRuntimeItem = {
|
|
56
|
+
column: string | PuriOrderByExpression;
|
|
57
|
+
order?: PuriOrderByDirection;
|
|
58
|
+
nulls?: PuriOrderByNulls;
|
|
59
|
+
};
|
|
60
|
+
type PuriOrderByRuntimeEntry = string | PuriOrderByExpression | PuriOrderByRuntimeItem;
|
|
61
|
+
|
|
43
62
|
function normalizeFuzzyOperator(operator?: string): FuzzyOperator {
|
|
44
63
|
const normalized = operator?.trim() ?? "<%";
|
|
45
64
|
const fuzzyOperator = FUZZY_OPERATORS.find((candidate) => candidate === normalized);
|
|
@@ -51,6 +70,42 @@ function normalizeFuzzyOperator(operator?: string): FuzzyOperator {
|
|
|
51
70
|
return fuzzyOperator;
|
|
52
71
|
}
|
|
53
72
|
|
|
73
|
+
function normalizeOrderByDirection(direction: PuriOrderByDirection = "asc"): PuriOrderByDirection {
|
|
74
|
+
if (direction !== "asc" && direction !== "desc") {
|
|
75
|
+
throw new Error(`Invalid order direction: ${direction}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return direction;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeOrderByNulls(nulls?: PuriOrderByNulls): PuriOrderByNulls | undefined {
|
|
82
|
+
if (nulls === undefined) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (nulls !== "first" && nulls !== "last") {
|
|
87
|
+
throw new Error(`Invalid order nulls: ${nulls}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return nulls;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatNullsSuffix(nulls?: PuriOrderByNulls): string {
|
|
94
|
+
if (!nulls) {
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return ` NULLS ${nulls.toUpperCase()}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isOrderByEntries(value: unknown): value is readonly PuriOrderByRuntimeEntry[] {
|
|
102
|
+
return Array.isArray(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isSqlExpression(value: PuriOrderByRuntimeEntry): value is PuriOrderByExpression {
|
|
106
|
+
return typeof value === "object" && "_type" in value && value._type === "sql_expression";
|
|
107
|
+
}
|
|
108
|
+
|
|
54
109
|
export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
|
|
55
110
|
private knexQuery: Knex.QueryBuilder;
|
|
56
111
|
private tableSpec: TableSpec | null = null;
|
|
@@ -837,19 +892,49 @@ export class Puri<TSchema, TTables extends Record<string, any>, TResult> {
|
|
|
837
892
|
|
|
838
893
|
// ORDER BY (SqlExpression으로도 할 수 있어야 함)
|
|
839
894
|
orderBy<TColumn extends ResultAvailableColumns<TTables, TResult>>(
|
|
840
|
-
column: TColumn |
|
|
841
|
-
direction
|
|
895
|
+
column: TColumn | PuriOrderByExpression,
|
|
896
|
+
direction?: PuriOrderByDirection,
|
|
897
|
+
nulls?: PuriOrderByNulls,
|
|
898
|
+
): this;
|
|
899
|
+
orderBy<TColumn extends ResultAvailableColumns<TTables, TResult>>(
|
|
900
|
+
columns: readonly PuriOrderByEntry<TColumn>[],
|
|
842
901
|
): this;
|
|
843
902
|
orderBy(
|
|
844
|
-
|
|
845
|
-
direction:
|
|
903
|
+
columnOrColumns: string | PuriOrderByExpression | readonly PuriOrderByRuntimeEntry[],
|
|
904
|
+
direction: PuriOrderByDirection = "asc",
|
|
905
|
+
nulls?: PuriOrderByNulls,
|
|
846
906
|
): this {
|
|
907
|
+
if (isOrderByEntries(columnOrColumns)) {
|
|
908
|
+
for (const entry of columnOrColumns) {
|
|
909
|
+
if (typeof entry === "string" || isSqlExpression(entry)) {
|
|
910
|
+
this.applyOrderBy(entry);
|
|
911
|
+
} else {
|
|
912
|
+
this.applyOrderBy(entry.column, entry.order, entry.nulls);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return this;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
this.applyOrderBy(columnOrColumns, direction, nulls);
|
|
919
|
+
return this;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
private applyOrderBy(
|
|
923
|
+
column: string | PuriOrderByExpression,
|
|
924
|
+
direction?: PuriOrderByDirection,
|
|
925
|
+
nulls?: PuriOrderByNulls,
|
|
926
|
+
): void {
|
|
927
|
+
const normalizedDirection = normalizeOrderByDirection(direction);
|
|
928
|
+
const normalizedNulls = normalizeOrderByNulls(nulls);
|
|
929
|
+
|
|
847
930
|
if (typeof column === "object") {
|
|
848
|
-
this.knexQuery.orderByRaw(
|
|
931
|
+
this.knexQuery.orderByRaw(
|
|
932
|
+
`${column._sql} ${normalizedDirection}${formatNullsSuffix(normalizedNulls)}`,
|
|
933
|
+
column._params,
|
|
934
|
+
);
|
|
849
935
|
} else {
|
|
850
|
-
this.knexQuery.orderBy(column,
|
|
936
|
+
this.knexQuery.orderBy(column, normalizedDirection, normalizedNulls);
|
|
851
937
|
}
|
|
852
|
-
return this;
|
|
853
938
|
}
|
|
854
939
|
|
|
855
940
|
forUpdate(): this {
|
|
@@ -477,3 +477,73 @@ describe("Puri locking methods", () => {
|
|
|
477
477
|
expectTypeOf(result).resolves.toEqualTypeOf<MockSchema["users"]>();
|
|
478
478
|
});
|
|
479
479
|
});
|
|
480
|
+
|
|
481
|
+
describe("Puri orderBy methods", () => {
|
|
482
|
+
it("단일 컬럼 null ordering과 체이닝 타입을 지원한다", () => {
|
|
483
|
+
type Query = Puri<MockSchema, { users: MockSchema["users"] }, MockSchema["users"]>;
|
|
484
|
+
const query = {} as Query;
|
|
485
|
+
|
|
486
|
+
expectTypeOf(query.orderBy("id")).toEqualTypeOf<Query>();
|
|
487
|
+
expectTypeOf(query.orderBy("id", "asc")).toEqualTypeOf<Query>();
|
|
488
|
+
expectTypeOf(query.orderBy("id", "desc", "last")).toEqualTypeOf<Query>();
|
|
489
|
+
expectTypeOf(query.orderBy("users.department_id", "asc", "first")).toEqualTypeOf<Query>();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("여러 컬럼 orderBy item 배열을 지원한다", () => {
|
|
493
|
+
type Query = Puri<MockSchema, { users: MockSchema["users"] }, MockSchema["users"]>;
|
|
494
|
+
const query = {} as Query;
|
|
495
|
+
|
|
496
|
+
expectTypeOf(query.orderBy(["name", "email"])).toEqualTypeOf<Query>();
|
|
497
|
+
expectTypeOf(
|
|
498
|
+
query.orderBy([
|
|
499
|
+
{ column: "name" },
|
|
500
|
+
{ column: "email", order: "asc" },
|
|
501
|
+
{ column: "users.department_id", order: "desc", nulls: "last" },
|
|
502
|
+
]),
|
|
503
|
+
).toEqualTypeOf<Query>();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it("select 결과 컬럼을 orderBy에 사용할 수 있다", () => {
|
|
507
|
+
type Result = { post_count: number };
|
|
508
|
+
type Query = Puri<MockSchema, { users: MockSchema["users"] }, Result>;
|
|
509
|
+
const query = {} as Query;
|
|
510
|
+
|
|
511
|
+
expectTypeOf(query.orderBy("post_count", "desc", "last")).toEqualTypeOf<Query>();
|
|
512
|
+
expectTypeOf(query.orderBy(["post_count"])).toEqualTypeOf<Query>();
|
|
513
|
+
expectTypeOf(
|
|
514
|
+
query.orderBy([{ column: "post_count", order: "desc", nulls: "last" }]),
|
|
515
|
+
).toEqualTypeOf<Query>();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("SqlExpression orderBy를 지원한다", () => {
|
|
519
|
+
type Query = Puri<MockSchema, { users: MockSchema["users"] }, MockSchema["users"]>;
|
|
520
|
+
const query = {} as Query;
|
|
521
|
+
const expression = Puri.rawNumber("COUNT(*)");
|
|
522
|
+
|
|
523
|
+
expectTypeOf(query.orderBy(expression, "desc", "last")).toEqualTypeOf<Query>();
|
|
524
|
+
expectTypeOf(query.orderBy([expression])).toEqualTypeOf<Query>();
|
|
525
|
+
expectTypeOf(
|
|
526
|
+
query.orderBy([{ column: expression, order: "desc", nulls: "first" }]),
|
|
527
|
+
).toEqualTypeOf<Query>();
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("잘못된 direction, nulls, column은 거부한다", () => {
|
|
531
|
+
type Query = Puri<MockSchema, { users: MockSchema["users"] }, MockSchema["users"]>;
|
|
532
|
+
const query = {} as Query;
|
|
533
|
+
|
|
534
|
+
// @ts-expect-error direction은 asc/desc만 허용한다.
|
|
535
|
+
query.orderBy("id", "ascending");
|
|
536
|
+
|
|
537
|
+
// @ts-expect-error nulls는 first/last만 허용한다.
|
|
538
|
+
query.orderBy("id", "asc", "middle");
|
|
539
|
+
|
|
540
|
+
// @ts-expect-error typed Puri에서는 존재하지 않는 컬럼을 허용하지 않는다.
|
|
541
|
+
query.orderBy("missing_column", "asc");
|
|
542
|
+
|
|
543
|
+
// @ts-expect-error 배열 item의 column도 typed Puri 컬럼 제약을 따른다.
|
|
544
|
+
query.orderBy([{ column: "missing_column", order: "asc" }]);
|
|
545
|
+
|
|
546
|
+
// @ts-expect-error string 배열도 typed Puri 컬럼 제약을 따른다.
|
|
547
|
+
query.orderBy(["name", "missing_column"]);
|
|
548
|
+
});
|
|
549
|
+
});
|
|
@@ -3,10 +3,11 @@ import { describe, expect, test } from "vitest";
|
|
|
3
3
|
import { Sonamu } from "../../api";
|
|
4
4
|
import { Entity } from "../../entity/entity";
|
|
5
5
|
import { EntityManager } from "../../entity/entity-manager";
|
|
6
|
-
import { type MigrationSet } from "../../types/types";
|
|
6
|
+
import { type MigrationIndex, type MigrationSet } from "../../types/types";
|
|
7
7
|
import {
|
|
8
8
|
generateAlterCode,
|
|
9
9
|
generateCreateCode,
|
|
10
|
+
getAlterIndexesTo,
|
|
10
11
|
setMigrationIndexDefaults,
|
|
11
12
|
} from "../code-generation";
|
|
12
13
|
import { getMigrationSetFromEntity } from "../migration-set";
|
|
@@ -387,3 +388,147 @@ describe("code-generation searchText/opclass DDL", () => {
|
|
|
387
388
|
expect(migration.formatted).toContain("USING gin(search_text gin_trgm_ops);");
|
|
388
389
|
});
|
|
389
390
|
});
|
|
391
|
+
|
|
392
|
+
describe("code-generation partial index DDL", () => {
|
|
393
|
+
test("unique partial index WHERE predicate를 출력해야 한다", async () => {
|
|
394
|
+
const migrationSet: MigrationSet = {
|
|
395
|
+
table: "partial_index_users",
|
|
396
|
+
columns: [
|
|
397
|
+
{ name: "id", type: "integer", nullable: false },
|
|
398
|
+
{ name: "email", type: "string", nullable: false },
|
|
399
|
+
{ name: "deleted_at", type: "date", nullable: true },
|
|
400
|
+
],
|
|
401
|
+
indexes: [
|
|
402
|
+
{
|
|
403
|
+
type: "unique",
|
|
404
|
+
name: "partial_index_users_email_active_unique",
|
|
405
|
+
columns: [{ name: "email" }],
|
|
406
|
+
nullsNotDistinct: true,
|
|
407
|
+
where: "deleted_at IS NULL",
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
foreigns: [],
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const [migration] = await generateCreateCode(migrationSet);
|
|
414
|
+
|
|
415
|
+
expect(migration.formatted).toContain(
|
|
416
|
+
"CREATE UNIQUE INDEX partial_index_users_email_active_unique ON",
|
|
417
|
+
);
|
|
418
|
+
expect(migration.formatted).toContain("NULLS NOT DISTINCT WHERE deleted_at IS NULL;");
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("vector partial index WHERE predicate를 출력해야 한다", async () => {
|
|
422
|
+
const migrationSet: MigrationSet = {
|
|
423
|
+
table: "partial_index_vectors",
|
|
424
|
+
columns: [
|
|
425
|
+
{ name: "id", type: "integer", nullable: false },
|
|
426
|
+
{ name: "embedding", type: "vector", dimensions: 1536, nullable: true },
|
|
427
|
+
],
|
|
428
|
+
indexes: [
|
|
429
|
+
{
|
|
430
|
+
type: "hnsw",
|
|
431
|
+
name: "partial_index_vectors_embedding_hnsw",
|
|
432
|
+
columns: [{ name: "embedding", vectorOps: "vector_cosine_ops" }],
|
|
433
|
+
where: "embedding IS NOT NULL",
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
foreigns: [],
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const [migration] = await generateCreateCode(migrationSet);
|
|
440
|
+
|
|
441
|
+
expect(migration.formatted).toContain(
|
|
442
|
+
"USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64) WHERE embedding IS NOT NULL",
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("pgroonga partial index WHERE predicate를 출력해야 한다", async () => {
|
|
447
|
+
const entity = await registerEntity({
|
|
448
|
+
id: "CodeGenerationPgroongaPartial",
|
|
449
|
+
table: "code_generation_pgroonga_partial",
|
|
450
|
+
props: [
|
|
451
|
+
{ name: "id", type: "integer" },
|
|
452
|
+
{ name: "title", type: "string" },
|
|
453
|
+
{ name: "deleted_at", type: "date", nullable: true },
|
|
454
|
+
],
|
|
455
|
+
indexes: [
|
|
456
|
+
{
|
|
457
|
+
type: "index",
|
|
458
|
+
name: "code_generation_pgroonga_partial_title_index",
|
|
459
|
+
using: "pgroonga",
|
|
460
|
+
columns: [{ name: "title" }],
|
|
461
|
+
where: "deleted_at IS NULL",
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const [migration] = await generateCreateCode(getMigrationSetFromEntity(entity));
|
|
467
|
+
|
|
468
|
+
expect(migration.formatted).toContain(
|
|
469
|
+
"USING pgroonga (title) WITH (tokenizer='TokenMecab') WHERE deleted_at IS NULL;",
|
|
470
|
+
);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("partial index predicate의 바깥 괄호 차이는 alter diff에서 no-op이어야 한다", () => {
|
|
474
|
+
const entityIndex: MigrationIndex = {
|
|
475
|
+
type: "index",
|
|
476
|
+
name: "partial_index_users_email_active_index",
|
|
477
|
+
columns: [{ name: "email" }],
|
|
478
|
+
where: "deleted_at IS NULL",
|
|
479
|
+
};
|
|
480
|
+
const dbIndex: MigrationIndex = {
|
|
481
|
+
...setMigrationIndexDefaults(entityIndex),
|
|
482
|
+
where: "(deleted_at IS NULL)",
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const alterIndexesTo = getAlterIndexesTo([entityIndex], [dbIndex]);
|
|
486
|
+
|
|
487
|
+
expect(alterIndexesTo.add).toHaveLength(0);
|
|
488
|
+
expect(alterIndexesTo.drop).toHaveLength(0);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("partial index predicate 변경은 alter diff에서 drop/add 대상이어야 한다", async () => {
|
|
492
|
+
const previousIndex: MigrationIndex = {
|
|
493
|
+
type: "index",
|
|
494
|
+
name: "partial_index_users_email_active_index",
|
|
495
|
+
columns: [{ name: "email" }],
|
|
496
|
+
where: "deleted_at IS NULL",
|
|
497
|
+
};
|
|
498
|
+
const nextIndex: MigrationIndex = {
|
|
499
|
+
...previousIndex,
|
|
500
|
+
where: "archived_at IS NULL",
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const alterIndexesTo = getAlterIndexesTo(
|
|
504
|
+
[nextIndex],
|
|
505
|
+
[setMigrationIndexDefaults(previousIndex)],
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
expect(alterIndexesTo.add).toHaveLength(1);
|
|
509
|
+
expect(alterIndexesTo.drop).toHaveLength(1);
|
|
510
|
+
|
|
511
|
+
const entitySet: MigrationSet = {
|
|
512
|
+
table: "partial_index_users",
|
|
513
|
+
columns: [
|
|
514
|
+
{ name: "id", type: "integer", nullable: false },
|
|
515
|
+
{ name: "email", type: "string", nullable: false },
|
|
516
|
+
{ name: "deleted_at", type: "date", nullable: true },
|
|
517
|
+
{ name: "archived_at", type: "date", nullable: true },
|
|
518
|
+
],
|
|
519
|
+
indexes: [nextIndex],
|
|
520
|
+
foreigns: [],
|
|
521
|
+
};
|
|
522
|
+
const dbSet: MigrationSet = {
|
|
523
|
+
...entitySet,
|
|
524
|
+
indexes: [setMigrationIndexDefaults(previousIndex)],
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const [migration] = await generateAlterCode(entitySet, dbSet);
|
|
528
|
+
|
|
529
|
+
expect(migration.formatted).toContain(
|
|
530
|
+
'table.dropIndex(["email"], "partial_index_users_email_active_index")',
|
|
531
|
+
);
|
|
532
|
+
expect(migration.formatted).toContain("WHERE archived_at IS NULL;");
|
|
533
|
+
});
|
|
534
|
+
});
|
|
@@ -963,6 +963,7 @@ function genIndexDefinition(index: MigrationIndex, table: string): string {
|
|
|
963
963
|
: "";
|
|
964
964
|
|
|
965
965
|
const usingClause = index.using === undefined ? "" : `USING ${index.using}`;
|
|
966
|
+
const whereClause = getIndexWhereClause(index);
|
|
966
967
|
|
|
967
968
|
return `await knex.raw(
|
|
968
969
|
\`CREATE ${methodMap[index.type]} ${index.name} ON ${table} ${usingClause}(${index.columns
|
|
@@ -982,12 +983,13 @@ function genIndexDefinition(index: MigrationIndex, table: string): string {
|
|
|
982
983
|
col.nullsFirst === undefined ? "" : ` NULLS ${col.nullsFirst ? "FIRST" : "LAST"}`;
|
|
983
984
|
return `${col.name}${opclassClause}${sortOrderClause}${nullsFirstClause}`;
|
|
984
985
|
})
|
|
985
|
-
.join(", ")})${nullsNotDistinctClause};\`
|
|
986
|
+
.join(", ")})${nullsNotDistinctClause}${whereClause};\`
|
|
986
987
|
);`;
|
|
987
988
|
}
|
|
988
989
|
|
|
989
990
|
function genPgroongaIndexDefinition(index: MigrationIndex, table: string) {
|
|
990
991
|
const entity = EntityManager.getByTable(table);
|
|
992
|
+
const whereClause = getIndexWhereClause(index);
|
|
991
993
|
|
|
992
994
|
// 복합 인덱스인 경우 ARRAY 사용
|
|
993
995
|
const columnClause = (() => {
|
|
@@ -1001,7 +1003,7 @@ function genPgroongaIndexDefinition(index: MigrationIndex, table: string) {
|
|
|
1001
1003
|
})();
|
|
1002
1004
|
|
|
1003
1005
|
return `await knex.raw(
|
|
1004
|
-
\`CREATE INDEX ${index.name} ON ${table} USING pgroonga (${columnClause}) WITH (tokenizer='TokenMecab');\`
|
|
1006
|
+
\`CREATE INDEX ${index.name} ON ${table} USING pgroonga (${columnClause}) WITH (tokenizer='TokenMecab')${whereClause};\`
|
|
1005
1007
|
)`;
|
|
1006
1008
|
}
|
|
1007
1009
|
|
|
@@ -1035,23 +1037,29 @@ function getPgroongaColumnOption(column: EntityProp) {
|
|
|
1035
1037
|
function genVectorIndexDefinition(index: MigrationIndex, table: string): string {
|
|
1036
1038
|
const column = index.columns[0];
|
|
1037
1039
|
const vectorOps = getIndexColumnOpclass(column) ?? "vector_cosine_ops";
|
|
1040
|
+
const whereClause = getIndexWhereClause(index);
|
|
1038
1041
|
|
|
1039
1042
|
// HNSW (Hierarchical Navigable Small World) - 권장: 빠른 검색, 높은 정확도
|
|
1040
1043
|
if (index.type === "hnsw") {
|
|
1041
1044
|
const m = index.m ?? 16;
|
|
1042
1045
|
const efConstruction = index.efConstruction ?? 64;
|
|
1043
|
-
return `await knex.raw(\`CREATE INDEX ${index.name} ON ${table} USING hnsw (${column.name} ${vectorOps}) WITH (m = ${m}, ef_construction = ${efConstruction})\`);`;
|
|
1046
|
+
return `await knex.raw(\`CREATE INDEX ${index.name} ON ${table} USING hnsw (${column.name} ${vectorOps}) WITH (m = ${m}, ef_construction = ${efConstruction})${whereClause}\`);`;
|
|
1044
1047
|
}
|
|
1045
1048
|
|
|
1046
1049
|
// IVFFlat (Inverted File with Flat Compression) - 대용량, 비용 중요 시
|
|
1047
1050
|
if (index.type === "ivfflat") {
|
|
1048
1051
|
const lists = index.lists ?? 100;
|
|
1049
|
-
return `await knex.raw(\`CREATE INDEX ${index.name} ON ${table} USING ivfflat (${column.name} ${vectorOps}) WITH (lists = ${lists})\`);`;
|
|
1052
|
+
return `await knex.raw(\`CREATE INDEX ${index.name} ON ${table} USING ivfflat (${column.name} ${vectorOps}) WITH (lists = ${lists})${whereClause}\`);`;
|
|
1050
1053
|
}
|
|
1051
1054
|
|
|
1052
1055
|
throw new Error(`Unknown raw SQL index type: ${index.type}`);
|
|
1053
1056
|
}
|
|
1054
1057
|
|
|
1058
|
+
function getIndexWhereClause(index: MigrationIndex): string {
|
|
1059
|
+
const where = normalizeIndexWherePredicate(index.where);
|
|
1060
|
+
return where ? ` WHERE ${where}` : "";
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1055
1063
|
/**
|
|
1056
1064
|
* 테이블 생성하는 케이스 - FK 생성
|
|
1057
1065
|
*/
|
|
@@ -1602,9 +1610,11 @@ export function getAlterIndexesTo(entityIndexes: MigrationIndex[], dbIndexes: Mi
|
|
|
1602
1610
|
.join("//");
|
|
1603
1611
|
};
|
|
1604
1612
|
|
|
1613
|
+
const normalizedEntityIndexes = entityIndexes.map(setMigrationIndexDefaults);
|
|
1614
|
+
const normalizedDbIndexes = dbIndexes.map(setMigrationIndexDefaults);
|
|
1605
1615
|
const extraIndexes = {
|
|
1606
|
-
db: diff(
|
|
1607
|
-
entity: diff(
|
|
1616
|
+
db: diff(normalizedDbIndexes, normalizedEntityIndexes, identity),
|
|
1617
|
+
entity: diff(normalizedEntityIndexes, normalizedDbIndexes, identity),
|
|
1608
1618
|
};
|
|
1609
1619
|
if (extraIndexes.entity.length > 0) {
|
|
1610
1620
|
indexesTo.add = indexesTo.add.concat(extraIndexes.entity);
|
|
@@ -1632,6 +1642,7 @@ export function setMigrationIndexDefaults(index: MigrationIndex): MigrationIndex
|
|
|
1632
1642
|
const isVectorIndex = index.type === "hnsw" || index.type === "ivfflat";
|
|
1633
1643
|
const supportsOrdering = !isVectorIndex && (!index.using || index.using === "btree");
|
|
1634
1644
|
const normalizedUsing = isVectorIndex ? index.using : (index.using ?? "btree");
|
|
1645
|
+
const normalizedWhere = normalizeIndexWherePredicate(index.where);
|
|
1635
1646
|
|
|
1636
1647
|
return {
|
|
1637
1648
|
...index,
|
|
@@ -1647,9 +1658,77 @@ export function setMigrationIndexDefaults(index: MigrationIndex): MigrationIndex
|
|
|
1647
1658
|
})),
|
|
1648
1659
|
nullsNotDistinct: index.nullsNotDistinct ?? false,
|
|
1649
1660
|
...(normalizedUsing ? { using: normalizedUsing } : {}),
|
|
1661
|
+
...(normalizedWhere ? { where: normalizedWhere } : {}),
|
|
1650
1662
|
};
|
|
1651
1663
|
}
|
|
1652
1664
|
|
|
1665
|
+
function normalizeIndexWherePredicate(where: string | undefined): string | undefined {
|
|
1666
|
+
if (!where) {
|
|
1667
|
+
return undefined;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
const trimmed = where.trim();
|
|
1671
|
+
if (trimmed.length === 0) {
|
|
1672
|
+
return undefined;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
if (trimmed.startsWith("(") && trimmed.endsWith(")")) {
|
|
1676
|
+
const closeIndex = findMatchingParenthesisInSql(trimmed, 0);
|
|
1677
|
+
if (closeIndex === trimmed.length - 1) {
|
|
1678
|
+
return trimmed.slice(1, -1).trim();
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
return trimmed;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
function findMatchingParenthesisInSql(source: string, openIndex: number): number {
|
|
1686
|
+
let depth = 0;
|
|
1687
|
+
let inSingleQuote = false;
|
|
1688
|
+
let inDoubleQuote = false;
|
|
1689
|
+
|
|
1690
|
+
for (let index = openIndex; index < source.length; index += 1) {
|
|
1691
|
+
const char = source[index];
|
|
1692
|
+
const nextChar = source[index + 1];
|
|
1693
|
+
|
|
1694
|
+
if (char === "'" && !inDoubleQuote) {
|
|
1695
|
+
if (inSingleQuote && nextChar === "'") {
|
|
1696
|
+
index += 1;
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
inSingleQuote = !inSingleQuote;
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
if (char === '"' && !inSingleQuote) {
|
|
1704
|
+
if (inDoubleQuote && nextChar === '"') {
|
|
1705
|
+
index += 1;
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
inDoubleQuote = !inDoubleQuote;
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
if (char === "(") {
|
|
1717
|
+
depth += 1;
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
if (char === ")") {
|
|
1722
|
+
depth -= 1;
|
|
1723
|
+
if (depth === 0) {
|
|
1724
|
+
return index;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
return -1;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1653
1732
|
/**
|
|
1654
1733
|
* 테이블 변경 케이스 - Foreign Key 변경
|
|
1655
1734
|
*/
|
|
@@ -56,6 +56,7 @@ type PgIndex = {
|
|
|
56
56
|
nulls_not_distinct: boolean;
|
|
57
57
|
column_order: number;
|
|
58
58
|
index_definition: string;
|
|
59
|
+
predicate?: string | null;
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
type PgForeign = {
|
|
@@ -203,6 +204,7 @@ class PostgreSQLSchemaReaderClass {
|
|
|
203
204
|
})),
|
|
204
205
|
|
|
205
206
|
nullsNotDistinct: firstIndex.nulls_not_distinct,
|
|
207
|
+
...(firstIndex.predicate ? { where: firstIndex.predicate } : {}),
|
|
206
208
|
...(using ? { using } : {}),
|
|
207
209
|
...this.parseVectorIndexOptions(restoredIndexType, parsedIndexDefinition.withOptions),
|
|
208
210
|
};
|
|
@@ -310,7 +312,8 @@ class PostgreSQLSchemaReaderClass {
|
|
|
310
312
|
END AS sort_order,
|
|
311
313
|
ix.indnullsnotdistinct AS nulls_not_distinct,
|
|
312
314
|
u.ord AS column_order,
|
|
313
|
-
pg_get_indexdef(ix.indexrelid) AS index_definition
|
|
315
|
+
pg_get_indexdef(ix.indexrelid) AS index_definition,
|
|
316
|
+
pg_get_expr(ix.indpred, ix.indrelid) AS predicate
|
|
314
317
|
FROM pg_class t
|
|
315
318
|
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
316
319
|
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
@@ -147,15 +147,23 @@ SD("enum.UserRole.admin"); // → "관리자"
|
|
|
147
147
|
|
|
148
148
|
## localizedColumn
|
|
149
149
|
|
|
150
|
-
Returns the value for the current locale when locale-specific columns exist in the DB:
|
|
150
|
+
Returns the value for the current locale when locale-specific columns or locale maps exist in the DB:
|
|
151
151
|
|
|
152
152
|
```typescript
|
|
153
153
|
import { localizedColumn } from "@/i18n/sd.generated";
|
|
154
154
|
|
|
155
155
|
// DB: { name: "태그", name_ko: "태그", name_en: "Tag" }
|
|
156
156
|
localizedColumn(tag, "name"); // → "태그" (ko) / "Tag" (en)
|
|
157
|
+
|
|
158
|
+
// DB: { name: { ko: ["태그"], en: ["Tag"] } }
|
|
159
|
+
localizedColumn(tag, "name"); // → ["태그"] (ko) / ["Tag"] (en)
|
|
157
160
|
```
|
|
158
161
|
|
|
162
|
+
- `string[]` values are returned as arrays instead of being stringified.
|
|
163
|
+
- Unsupported Context locales use `defaultLocale` before lookup.
|
|
164
|
+
- Direct suffix/base scalar values such as numbers are stringified for compatibility.
|
|
165
|
+
- Locale map values support only `string` and `string[]`.
|
|
166
|
+
|
|
159
167
|
## Sonamu UI Management
|
|
160
168
|
|
|
161
169
|
`http://localhost:34900/sonamu-ui` → i18n tab:
|