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.
@@ -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+IHtcbiAgcmV0dXJuIHZhbHVlICE9PSBudWxsICYmIHZhbHVlICE9PSB1bmRlZmluZWQ7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBleGhhdXN0aXZlKF9wYXJhbTogbmV2ZXIpIHtcbiAgdGhyb3cgbmV3IEVycm9yKGBleGhhdXN0aXZlYCk7XG59XG5cbi8vIOydvOuwmCDrsoTsoIRcbmV4cG9ydCBmdW5jdGlvbiBhc3NlcnRFeGlzdHM8VD4odmFsdWU6IFQgfCBudWxsIHwgdW5kZWZpbmVkLCBtZXNzYWdlPzogc3RyaW5nKTogVCB7XG4gIGlmICh2YWx1ZSA9PT0gbnVsbCB8fCB2YWx1ZSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKG1lc3NhZ2UgPz8gXCJWYWx1ZSBtdXN0IGV4aXN0XCIpO1xuICB9XG4gIHJldHVybiB2YWx1ZTtcbn1cblxuLy8gbnVsbOunjCDssrTtgaxcbmV4cG9ydCBmdW5jdGlvbiBhc3NlcnROb3ROdWxsPFQ+KHZhbHVlOiBUIHwgbnVsbCwgbWVzc2FnZT86IHN0cmluZyk6IFQge1xuICBpZiAodmFsdWUgPT09IG51bGwpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IobWVzc2FnZSA/PyBcIlZhbHVlIG11c3Qgbm90IGJlIG51bGxcIik7XG4gIH1cbiAgcmV0dXJuIHZhbHVlO1xufVxuLy8gdW5kZWZpbmVk66eMIOyytO2BrFxuZXhwb3J0IGZ1bmN0aW9uIGFzc2VydERlZmluZWQ8VD4odmFsdWU6IFQgfCB1bmRlZmluZWQsIG1lc3NhZ2U/OiBzdHJpbmcpOiBUIHtcbiAgaWYgKHZhbHVlID09PSB1bmRlZmluZWQpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IobWVzc2FnZSA/PyBcIlZhbHVlIG11c3QgYmUgZGVmaW5lZFwiKTtcbiAgfVxuICByZXR1cm4gdmFsdWU7XG59XG5cbi8vIGxvZGFzaCBpbnRlcnNlY3Rpb25CeSDrjIDssrRcbmV4cG9ydCBmdW5jdGlvbiBpbnRlcnNlY3Rpb25CeTxULCBLPihcbiAgYXJyMTogcmVhZG9ubHkgVFtdLFxuICBhcnIyOiByZWFkb25seSBUW10sXG4gIGl0ZXJhdGVlOiAoaXRlbTogVCkgPT4gSyxcbik6IFRbXSB7XG4gIGNvbnN0IGFycjJLZXlzID0gbmV3IFNldChhcnIyLm1hcChpdGVyYXRlZSkpO1xuICByZXR1cm4gYXJyMS5maWx0ZXIoKGl0ZW0pID0+IGFycjJLZXlzLmhhcyhpdGVyYXRlZShpdGVtKSkpO1xufVxuLy8gbG9kYXNoIGRpZmZlcmVuY2VXaXRoIOuMgOyytFxuZXhwb3J0IGZ1bmN0aW9uIGRpZmZlcmVuY2VXaXRoPFQ+KFxuICBhcnIxOiByZWFkb25seSBUW10sXG4gIGFycjI6IHJlYWRvbmx5IFRbXSxcbiAgY29tcGFyYXRvcjogKGE6IFQsIGI6IFQpID0+IGJvb2xlYW4sXG4pOiBUW10ge1xuICByZXR1cm4gYXJyMS5maWx0ZXIoKGl0ZW1BKSA9PiAhYXJyMi5zb21lKChpdGVtQikgPT4gY29tcGFyYXRvcihpdGVtQSwgaXRlbUIpKSk7XG59XG5cbi8vIGJpb21lLWlnbm9yZSBsaW50L3N1c3BpY2lvdXMvbm9FeHBsaWNpdEFueTogZHluYW1pYyBwcm9wZXJ0eSBhY2Nlc3NcbmV4cG9ydCBmdW5jdGlvbiBtZXJnZTxUIGV4dGVuZHMgUmVjb3JkPHN0cmluZywgYW55Pj4oZGVmYXVsdE9iajogVCwgdXNlck9iajogVCk6IFQge1xuICAvLyDsm5Drs7gg67O07KG07J2EIOychO2VtCBkZWZhdWx0T2JqIOuzteyCrFxuICBjb25zdCByZXN1bHQgPSB7IC4uLmRlZmF1bHRPYmogfTtcblxuICAvLyB1c2VyT2Jq7J2YIOqwgSDsho3shLHsnYQg7Iic7ZqMXG4gIGZvciAoY29uc3Qga2V5IGluIHVzZXJPYmopIHtcbiAgICAvLyB1c2VyT2Jq7J2YIG93biBwcm9wZXJ0eeunjCDsspjrpqwgKO2UhOuhnO2GoO2DgOyehSDssrTsnbgg7KCc7Jm4KVxuICAgIGlmIChPYmplY3QuaGFzT3duKHVzZXJPYmosIGtleSkpIHtcbiAgICAgIGNvbnN0IHVzZXJWYWx1ZSA9IHVzZXJPYmpba2V5XTtcbiAgICAgIGNvbnN0IGRlZmF1bHRWYWx1ZSA9IHJlc3VsdFtrZXldO1xuXG4gICAgICAvLyDrkZAg6rCS7J20IOuqqOuRkCDqsJ3ssrTsnbTqs6AsIOuwsOyXtOydtCDslYTri4wg6rK97JqwIOyerOq3gOyggeycvOuhnCDrs5HtlalcbiAgICAgIGlmIChpc1BsYWluT2JqZWN0KHVzZXJWYWx1ZSkgJiYgaXNQbGFpbk9iamVjdChkZWZhdWx0VmFsdWUpKSB7XG4gICAgICAgIHJlc3VsdFtrZXldID0gbWVyZ2UoZGVmYXVsdFZhbHVlLCB1c2VyVmFsdWUpO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgLy8g6re4IOyZuOydmCDqsr3smrAgdXNlck9iauydmCDqsJLsnLzroZwg642u7Ja07JOw6riwXG4gICAgICAgIHJlc3VsdFtrZXldID0gdXNlclZhbHVlO1xuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIHJldHVybiByZXN1bHQ7XG59XG5cbi8vIHBsYWluIG9iamVjdCDtjJDrs4Qg7Zes7Y28IO2VqOyImFxuLy8gKOuwsOyXtCwgbnVsbCwgRGF0ZSDrk7HsnYQg7KCc7Jm47ZWcIOyInOyImCDqsJ3ssrTrp4wgdHJ1ZSlcbmV4cG9ydCBmdW5jdGlvbiBpc1BsYWluT2JqZWN0KHZhbHVlOiB1bmtub3duKTogdmFsdWUgaXMgUmVjb3JkPHN0cmluZywgdW5rbm93bj4ge1xuICByZXR1cm4gKFxuICAgIHZhbHVlICE9PSBudWxsICYmXG4gICAgdHlwZW9mIHZhbHVlID09PSBcIm9iamVjdFwiICYmXG4gICAgIUFycmF5LmlzQXJyYXkodmFsdWUpICYmXG4gICAgT2JqZWN0LnByb3RvdHlwZS50b1N0cmluZy5jYWxsKHZhbHVlKSA9PT0gXCJbb2JqZWN0IE9iamVjdF1cIlxuICApO1xufVxuXG4vLyBDb252ZXJ0IEZhc3RpZnkgaGVhZGVycyB0byBzdGFuZGFyZCBIZWFkZXJzIG9iamVjdFxuZXhwb3J0IGZ1bmN0aW9uIGNvbnZlcnRGYXN0aWZ5SGVhZGVyc1RvU3RhbmRhcmQoaGVhZGVyczogRmFzdGlmeVJlcXVlc3RbXCJoZWFkZXJzXCJdKTogSGVhZGVycyB7XG4gIGNvbnN0IGhlYWRlcnNPYmogPSBuZXcgSGVhZGVycygpO1xuICBPYmplY3QuZW50cmllcyhoZWFkZXJzKS5mb3JFYWNoKChba2V5LCB2YWx1ZV0pID0+IHtcbiAgICBpZiAodmFsdWUpIGhlYWRlcnNPYmouYXBwZW5kKGtleSwgdmFsdWUudG9TdHJpbmcoKSk7XG4gIH0pO1xuICByZXR1cm4gaGVhZGVyc09iajtcbn1cbiJdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFPQSxTQUFnQixrQkFBZ0M7Q0FDOUMsTUFBTSxjQUFjLGlCQUFpQjtBQUNyQyxRQUFPLEtBQUssUUFBUSxZQUFZOztBQUdsQyxTQUFnQixrQkFBZ0M7Q0FHOUMsTUFBTSxnQkFBZ0IsUUFBUSxJQUFJLHVCQUF1QixRQUFRLElBQUk7QUFDckUsS0FBSSxZQUFZLGNBQWMsRUFBRTtBQUM5QixTQUFPOztBQUdULEtBQUksWUFBWSxRQUFRLElBQUksa0JBQWtCLEVBQUU7QUFDOUMsU0FBTyxRQUFRLEtBQUssQ0FBQyxNQUFNLEtBQUssSUFBSSxDQUFDLEtBQUssS0FBSyxJQUFJOztDQUdyRCxNQUFNLGlCQUFpQixLQUFLLEtBQUssUUFBUSxLQUFLLEVBQUUsZUFBZTtBQUMvRCxLQUFJLEdBQUcsV0FBVyxlQUFlLEVBQUU7QUFDakMsU0FBTyxRQUFRLEtBQUssQ0FBQyxNQUFNLEtBQUssSUFBSSxDQUFDLEtBQUssS0FBSyxJQUFJOztDQUdyRCxNQUFNLFdBQVcsT0FBTyxLQUFLO0NBQzdCLElBQUksTUFBTSxLQUFLLFFBQVEsU0FBUztBQUNoQyxLQUFJLElBQUksU0FBUyxVQUFVLEVBQUU7QUFDM0IsUUFBTSxJQUFJLE1BQU0sVUFBVSxDQUFDOztBQUc3QixJQUFHO0FBQ0QsTUFBSSxHQUFHLFdBQVcsS0FBSyxLQUFLLEtBQUssZ0JBQWdCLENBQUMsRUFBRTtBQUNsRCxVQUFPLElBQUksTUFBTSxLQUFLLElBQUksQ0FBQyxLQUFLLEtBQUssSUFBSTs7QUFFM0MsUUFBTSxJQUFJLE1BQU0sS0FBSyxJQUFJLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDLEtBQUssS0FBSyxJQUFJO1VBQzlDLElBQUksTUFBTSxLQUFLLElBQUksQ0FBQyxTQUFTO0FBQ3RDLE9BQU0sSUFBSSxNQUFNLHNDQUFzQzs7QUFHeEQsU0FBZ0IsWUFBZSxPQUFtQztBQUNoRSxRQUFPLFVBQVUsUUFBUSxVQUFVOztBQUdyQyxTQUFnQixXQUFXLFFBQWU7QUFDeEMsT0FBTSxJQUFJLE1BQU0sYUFBYTs7QUFJL0IsU0FBZ0IsYUFBZ0IsT0FBNkIsU0FBcUI7QUFDaEYsS0FBSSxVQUFVLFFBQVEsVUFBVSxXQUFXO0FBQ3pDLFFBQU0sSUFBSSxNQUFNLFdBQVcsbUJBQW1COztBQUVoRCxRQUFPOztBQUlULFNBQWdCLGNBQWlCLE9BQWlCLFNBQXFCO0FBQ3JFLEtBQUksVUFBVSxNQUFNO0FBQ2xCLFFBQU0sSUFBSSxNQUFNLFdBQVcseUJBQXlCOztBQUV0RCxRQUFPOztBQUdULFNBQWdCLGNBQWlCLE9BQXNCLFNBQXFCO0FBQzFFLEtBQUksVUFBVSxXQUFXO0FBQ3ZCLFFBQU0sSUFBSSxNQUFNLFdBQVcsd0JBQXdCOztBQUVyRCxRQUFPOztBQUlULFNBQWdCLGVBQ2QsTUFDQSxNQUNBLFVBQ0s7Q0FDTCxNQUFNLFdBQVcsSUFBSSxJQUFJLEtBQUssSUFBSSxTQUFTLENBQUM7QUFDNUMsUUFBTyxLQUFLLFFBQVEsU0FBUyxTQUFTLElBQUksU0FBUyxLQUFLLENBQUMsQ0FBQzs7QUFHNUQsU0FBZ0IsZUFDZCxNQUNBLE1BQ0EsWUFDSztBQUNMLFFBQU8sS0FBSyxRQUFRLFVBQVUsQ0FBQyxLQUFLLE1BQU0sVUFBVSxXQUFXLE9BQU8sTUFBTSxDQUFDLENBQUM7O0FBSWhGLFNBQWdCLE1BQXFDLFlBQWUsU0FBZTtDQUVqRixNQUFNLFNBQVMsRUFBRSxHQUFHLFlBQVk7QUFHaEMsTUFBSyxNQUFNLE9BQU8sU0FBUztBQUV6QixNQUFJLE9BQU8sT0FBTyxTQUFTLElBQUksRUFBRTtHQUMvQixNQUFNLFlBQVksUUFBUTtHQUMxQixNQUFNLGVBQWUsT0FBTztBQUc1QixPQUFJLGNBQWMsVUFBVSxJQUFJLGNBQWMsYUFBYSxFQUFFO0FBQzNELFdBQU8sT0FBTyxNQUFNLGNBQWMsVUFBVTtVQUN2QztBQUVMLFdBQU8sT0FBTzs7OztBQUtwQixRQUFPOztBQUtULFNBQWdCLGNBQWMsT0FBa0Q7QUFDOUUsUUFDRSxVQUFVLFFBQ1YsT0FBTyxVQUFVLFlBQ2pCLENBQUMsTUFBTSxRQUFRLE1BQU0sSUFDckIsT0FBTyxVQUFVLFNBQVMsS0FBSyxNQUFNLEtBQUs7O0FBSzlDLFNBQWdCLGdDQUFnQyxTQUE2QztDQUMzRixNQUFNLGFBQWEsSUFBSSxTQUFTO0FBQ2hDLFFBQU8sUUFBUSxRQUFRLENBQUMsU0FBUyxDQUFDLEtBQUssV0FBVztBQUNoRCxNQUFJLE1BQU8sWUFBVyxPQUFPLEtBQUssTUFBTSxVQUFVLENBQUM7R0FDbkQ7QUFDRixRQUFPIn0=
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.13",
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
  },
@@ -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 | SqlExpression<"number"> | SqlExpression<"string">,
841
- direction: "asc" | "desc",
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
- column: string | SqlExpression<"number"> | SqlExpression<"string">,
845
- direction: "asc" | "desc" = "asc",
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(`${column._sql} ${direction}`, column._params);
931
+ this.knexQuery.orderByRaw(
932
+ `${column._sql} ${normalizedDirection}${formatNullsSuffix(normalizedNulls)}`,
933
+ column._params,
934
+ );
849
935
  } else {
850
- this.knexQuery.orderBy(column, direction);
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(dbIndexes, entityIndexes.map(setMigrationIndexDefaults), identity),
1607
- entity: diff(entityIndexes.map(setMigrationIndexDefaults), dbIndexes, identity),
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: