libmodulor 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +154 -2
  3. package/dist/esm/apps/Helper/src/lib/project.js +6 -6
  4. package/dist/esm/apps/Helper/src/ucds/TestAppUCD.d.ts +2 -1
  5. package/dist/esm/apps/Helper/src/ucds/TestAppUCD.js +8 -0
  6. package/dist/esm/convention.d.ts +3 -0
  7. package/dist/esm/convention.js +3 -0
  8. package/dist/esm/dt/DataTypes.d.ts +1 -2
  9. package/dist/esm/dt/Validation.d.ts +2 -2
  10. package/dist/esm/dt/base/TBase.d.ts +2 -0
  11. package/dist/esm/dt/base/TBase.js +3 -0
  12. package/dist/esm/dt/base/TObject.d.ts +6 -4
  13. package/dist/esm/dt/base/TObject.js +5 -6
  14. package/dist/esm/dt/base/TString.d.ts +2 -1
  15. package/dist/esm/dt/base/TString.js +22 -0
  16. package/dist/esm/dt/final/TFile.d.ts +13 -4
  17. package/dist/esm/dt/final/TFile.js +70 -8
  18. package/dist/esm/dt/final/TFileExtension.d.ts +1 -1
  19. package/dist/esm/dt/final/TFileMimeType.d.ts +2 -6
  20. package/dist/esm/dt/final/TFileMimeType.js +0 -6
  21. package/dist/esm/dt/final/TFilePath.d.ts +7 -0
  22. package/dist/esm/dt/final/TFilePath.js +5 -1
  23. package/dist/esm/dt/index.d.ts +1 -1
  24. package/dist/esm/i18n/WordingManager.d.ts +2 -1
  25. package/dist/esm/i18n/WordingManager.js +23 -2
  26. package/dist/esm/i18n/index.d.ts +1 -1
  27. package/dist/esm/i18n/locales/de.d.ts +2 -0
  28. package/dist/esm/i18n/locales/de.js +62 -0
  29. package/dist/esm/i18n/locales/en.js +8 -0
  30. package/dist/esm/i18n/locales/es.d.ts +2 -0
  31. package/dist/esm/i18n/locales/es.js +62 -0
  32. package/dist/esm/i18n/locales/fr.js +8 -0
  33. package/dist/esm/i18n/types.d.ts +5 -4
  34. package/dist/esm/index.babel.d.ts +1 -0
  35. package/dist/esm/index.babel.js +1 -0
  36. package/dist/esm/index.node-stricli-cli.d.ts +2 -0
  37. package/dist/esm/index.node-stricli-cli.js +2 -0
  38. package/dist/esm/index.vite.d.ts +1 -1
  39. package/dist/esm/index.vite.js +1 -1
  40. package/dist/esm/index.webpack.d.ts +1 -0
  41. package/dist/esm/index.webpack.js +1 -0
  42. package/dist/esm/std/FSManager.d.ts +7 -5
  43. package/dist/esm/std/FSManager.js +5 -6
  44. package/dist/esm/std/FormDataBuilder.d.ts +2 -1
  45. package/dist/esm/std/I18nManager.d.ts +12 -0
  46. package/dist/esm/std/impl/NodeFSManager.js +4 -3
  47. package/dist/esm/std/impl/SimpleMapI18nManager.d.ts +6 -1
  48. package/dist/esm/std/impl/SimpleMapI18nManager.js +41 -12
  49. package/dist/esm/target/lib/react/StyleContextProvider.d.ts +1 -0
  50. package/dist/esm/target/lib/react/form.d.ts +1 -1
  51. package/dist/esm/target/lib/react/form.js +1 -0
  52. package/dist/esm/target/lib/react/useAction.d.ts +1 -1
  53. package/dist/esm/target/lib/react/useAction.js +13 -12
  54. package/dist/esm/target/lib/server/AuthenticationChecker.js +1 -1
  55. package/dist/esm/target/lib/server/PublicApiKeyChecker.js +1 -1
  56. package/dist/esm/target/lib/server/ServerRequestHandler.js +2 -2
  57. package/dist/esm/target/lib/server-express/funcs.js +4 -3
  58. package/dist/esm/target/lib/server-hono/funcs.js +2 -2
  59. package/dist/esm/target/lib/server-node/funcs.js +1 -1
  60. package/dist/esm/target/lib/web/input.d.ts +1 -0
  61. package/dist/esm/target/lib/web/input.js +5 -1
  62. package/dist/esm/target/node-stricli-cli/NodeStricliCLIManager.d.ts +12 -0
  63. package/dist/esm/target/node-stricli-cli/NodeStricliCLIManager.js +118 -0
  64. package/dist/esm/target/react-native-pure/UCFormField.js +2 -1
  65. package/dist/esm/target/react-native-pure/UCFormFieldControl.js +6 -4
  66. package/dist/esm/target/react-native-pure/UCFormFieldErr.d.ts +1 -1
  67. package/dist/esm/target/react-native-pure/UCFormFieldErr.js +4 -1
  68. package/dist/esm/target/react-native-pure/UCFormFieldHelp.d.ts +4 -0
  69. package/dist/esm/target/react-native-pure/UCFormFieldHelp.js +15 -0
  70. package/dist/esm/target/react-web-pure/UCFormField.js +2 -1
  71. package/dist/esm/target/react-web-pure/UCFormFieldErr.d.ts +1 -1
  72. package/dist/esm/target/react-web-pure/UCFormFieldErr.js +4 -1
  73. package/dist/esm/target/react-web-pure/UCFormFieldHelp.d.ts +4 -0
  74. package/dist/esm/target/react-web-pure/UCFormFieldHelp.js +14 -0
  75. package/dist/esm/testing/AppTester.js +4 -5
  76. package/dist/esm/testing/UCDefASTParser.d.ts +24 -6
  77. package/dist/esm/testing/impl/SimpleAppDocsEmitter/SimpleAppDocsEmitter.d.ts +7 -0
  78. package/dist/esm/testing/impl/SimpleAppDocsEmitter/SimpleAppDocsEmitter.js +59 -0
  79. package/dist/esm/testing/impl/SimpleAppDocsEmitter/markdown.d.ts +2 -0
  80. package/dist/esm/testing/impl/SimpleAppDocsEmitter/markdown.js +10 -0
  81. package/dist/esm/testing/impl/SimpleAppDocsEmitter/sequence-diagram.d.ts +2 -0
  82. package/dist/esm/testing/impl/SimpleAppDocsEmitter/sequence-diagram.js +92 -0
  83. package/dist/esm/testing/impl/SimpleAppDocsEmitter/tech-summary.d.ts +2 -0
  84. package/dist/esm/testing/impl/SimpleAppDocsEmitter/tech-summary.js +27 -0
  85. package/dist/esm/testing/impl/SimpleAppDocsEmitter/uc-summary.d.ts +2 -0
  86. package/dist/esm/testing/impl/SimpleAppDocsEmitter/uc-summary.js +63 -0
  87. package/dist/esm/testing/impl/SimpleAppDocsEmitter.js +4 -5
  88. package/dist/esm/testing/impl/TypeScriptLibUCDefASTParser.js +38 -11
  89. package/dist/esm/testing/impl/VitestAppTestSuiteRunner.d.ts +1 -1
  90. package/dist/esm/testing/impl/VitestAppTestSuiteRunner.js +17 -2
  91. package/dist/esm/testing/impl/newNodeAppTester.js +1 -1
  92. package/dist/esm/testing/opts.js +1 -1
  93. package/dist/esm/testing/uc-input.js +5 -2
  94. package/dist/esm/testing/workers/AppTestSuiteRunner.d.ts +2 -0
  95. package/dist/esm/testing/workers/UCExecutor.js +1 -1
  96. package/dist/esm/testing/workers/checkers/AppI18nChecker.d.ts +8 -2
  97. package/dist/esm/testing/workers/checkers/AppI18nChecker.js +44 -2
  98. package/dist/esm/testing/workers/checkers/UCDefSourcesChecker.js +12 -12
  99. package/dist/esm/uc/exec.d.ts +42 -21
  100. package/dist/esm/uc/exec.js +48 -13
  101. package/dist/esm/uc/impl/HTTPUCTransporter.js +2 -2
  102. package/dist/esm/uc/impl/KnexUCDataStore.js +2 -2
  103. package/dist/esm/uc/index.d.ts +0 -1
  104. package/dist/esm/uc/index.js +0 -1
  105. package/dist/esm/uc/input-field.d.ts +6 -4
  106. package/dist/esm/uc/input-field.js +4 -5
  107. package/dist/esm/uc/side-effect.d.ts +10 -8
  108. package/dist/esm/uc/side-effect.js +5 -6
  109. package/dist/esm/uc/workers/UCExecChecker.js +1 -1
  110. package/dist/esm/uc/workers/UCInputFilesProcessor.js +3 -3
  111. package/dist/esm/uc/workers/UCInputValidator.js +2 -1
  112. package/dist/esm/uc/workers/UCOutputFilesProcessor.js +1 -1
  113. package/dist/esm/utils/bundling/babel/plugin.d.ts +2 -0
  114. package/dist/esm/utils/bundling/babel/plugin.js +38 -0
  115. package/dist/esm/utils/bundling/funcs.d.ts +6 -0
  116. package/dist/esm/utils/bundling/funcs.js +20 -0
  117. package/dist/esm/utils/bundling/typescript.d.ts +3 -0
  118. package/dist/esm/utils/bundling/typescript.js +61 -0
  119. package/dist/esm/utils/bundling/vite/plugin.d.ts +6 -0
  120. package/dist/esm/utils/bundling/vite/plugin.js +17 -0
  121. package/dist/esm/utils/bundling/webpack/loader.d.ts +2 -0
  122. package/dist/esm/utils/bundling/webpack/loader.js +12 -0
  123. package/dist/esm/utils/http/HTTPRequestBuilder.js +1 -1
  124. package/dist/esm/utils/index.d.ts +1 -1
  125. package/dist/esm/utils/ioc/bindCommon.js +1 -1
  126. package/dist/esm/utils/terminal/fmt.js +1 -1
  127. package/dist/esm/utils/types/utility-types.d.ts +4 -0
  128. package/package.json +36 -12
  129. package/pnpm-workspace.yaml +1 -4
  130. package/tsconfig.build.examples.json +8 -0
  131. package/tsconfig.json +1 -0
  132. package/vitest.config.ts +16 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v0.25.0 (2025-12-21)
4
+
5
+ **Highlights**
6
+
7
+ - Added use case summary in the auto-generated app docs : In addition to the existing mermaid diagram, there is now a summary of the lifecyle, input and output in a dedicated table
8
+ - Example : https://github.com/c100k/libmodulor/tree/42fd11307cdd9f8cdec19b4de75d4959bc9f9a9e/examples/apps/Spotify#createalbum
9
+ - Improved the `File` data type
10
+ - Changed the property `path` to `uri` to conform to the `FormData` spec (this is a breaking change)
11
+ - Simplified the declaration by inlining the accepted types via `accept` instead of `type.allowed` (this is also a breaking change)
12
+ - Added `maxSizeInBytes` and `minSizeInBytes` for size validation
13
+ - Added `getConstraintsForHuman` to format constraints for the end user in the targets. For instance :
14
+ - For `FreeTextShort`, it displays `Max length: 150` 🇬🇧 when present
15
+ - For `File`, it displays `Max size: 8MB - Accepted types: application/png, application/jpg` 🇬🇧 when present
16
+ - Applied to the `react-native-pure` and `react-native-web` targets (see `UCFormFieldHelp.tsx`)
17
+
18
+ See all the changes here : https://github.com/c100k/libmodulor/compare/v0.24.0...master
19
+
20
+ ## v0.24.0 (2025-12-07)
21
+
22
+ **Highlights**
23
+
24
+ - Introduced `babel`, `vite` plugins and `webpack` loader => https://libmodulor.c100k.eu/docs/guides/bundle-target
25
+ - Introduced the `node-stricli-cli` target using @bloomberg's `stricli` library
26
+ - Introduced the `Playground` example, runnable locally, to play with all the features of `libmodulor` => https://libmodulor.c100k.eu/docs/examples/Playground
27
+ - Improved apps testing by making sure non-english languages are fully translated
28
+ - Improved apps testing performance by optimizing use cases testing using the `stream` transport
29
+ - Introduced `de` 🇩🇪 and `es` 🇪🇸 locales (see them in action in the `Playground`)
30
+
31
+ Some breaking changes but at the very low level. If your apps break, just follow the TypeScript errors and you should be good to go.
32
+
33
+ Also bumped React versions for the infamous [React2Shell vulnerability](https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components).
34
+
35
+ See all the changes here : https://github.com/c100k/libmodulor/compare/v0.23.0...master
36
+
3
37
  ## v0.23.0 (2025-11-11)
4
38
 
5
39
  See all the changes here : https://github.com/c100k/libmodulor/compare/v0.22.0...master
package/README.md CHANGED
@@ -7,14 +7,166 @@ A TypeScript library to create platform-agnostic applications.
7
7
 
8
8
  ## 🚀 Getting Started
9
9
 
10
- If you're discovering `libmodulor`, we recommend reading the [📖 Documentation](https://libmodulor.c100k.eu/docs) first. You'll find everything you need to get started : Concepts, Examples and Guides.
10
+ If you're discovering `libmodulor`, we recommend reading the [📖 Documentation](https://libmodulor.c100k.eu/docs) first.
11
+ You'll find everything you need to get started : Concepts, Examples and Guides.
11
12
 
12
13
  When you're ready, [🚀 Create a project](https://libmodulor.c100k.eu/docs/guides/create-project) and build the awesome idea you have in mind.
13
14
 
15
+ In the meantime, here is how to declare the four layers of `libmodulor`.
16
+
17
+ _These snippets are extracted from the [`Basic`](https://libmodulor.c100k.eu/docs/examples/Basic) example (check out the full example to get the full picture)._
18
+
19
+ ### App
20
+
21
+ ```ts
22
+ const appManifest = {
23
+ languageCodes: ['en', 'fr'],
24
+ name: 'Event',
25
+ ucReg: {
26
+ Register: {
27
+ action: 'Create',
28
+ icon: 'user',
29
+ name: 'Register',
30
+ },
31
+ },
32
+ } satisfies AppManifest;
33
+
34
+ const appI18n: AppI18n = {
35
+ en: {
36
+ ucif_email_label: 'Your email address',
37
+ ucif_firstname_label: 'Your firstname',
38
+ ucif_lastname_label: 'Your lastname',
39
+ ucof_id_label: 'Your registration #',
40
+ ucof_ticketNumber_label: 'Your ticket #',
41
+ },
42
+ fr: {
43
+ ucif_email_label: 'Votre adresse email',
44
+ ucif_firstname_label: 'Votre prénom',
45
+ ucif_lastname_label: 'Votre nom',
46
+ ucof_id_label: "Votre # d'inscription",
47
+ ucof_ticketNumber_label: 'Votre # de ticket',
48
+ },
49
+ };
50
+ ```
51
+
52
+ ### Use Case
53
+
54
+ ```ts
55
+ interface RegisterInput extends UCInput {
56
+ email: UCInputFieldValue<Email>;
57
+ firstname: UCInputFieldValue<PersonFirstname>;
58
+ lastname: UCInputFieldValue<PersonLastname>;
59
+ }
60
+
61
+ interface RegisterOPI0 extends AggregateOPI0 {
62
+ amount: Amount;
63
+ ticketNumber: TicketNumber;
64
+ }
65
+
66
+ @injectable()
67
+ class RegisterClientMain implements UCMain<RegisterInput, RegisterOPI0> {
68
+ constructor(@inject('UCManager') private ucManager: UCManager) {}
69
+
70
+ public async exec({
71
+ uc,
72
+ }: UCMainInput<RegisterInput, RegisterOPI0>): Promise<
73
+ UCOutput<RegisterOPI0>
74
+ > {
75
+ const { aggregateId } = await this.ucManager.persist(uc);
76
+
77
+ const amount: Amount = 99.99; // Should come from some catalog in a real application
78
+ const ticketNumber: TicketNumber = 1; // Should come from a safely auto-generated sequence in a real application
79
+
80
+ return new UCOutputBuilder<RegisterOPI0>()
81
+ .add({
82
+ amount,
83
+ id: aggregateId,
84
+ ticketNumber,
85
+ })
86
+ .get();
87
+ }
88
+ }
89
+
90
+ const RegisterUCD: UCDef<RegisterInput, RegisterOPI0> = {
91
+ io: {
92
+ i: {
93
+ fields: {
94
+ email: {
95
+ type: new TEmail(),
96
+ },
97
+ firstname: {
98
+ type: new TPersonFirstname(),
99
+ },
100
+ lastname: {
101
+ type: new TPersonLastname(),
102
+ },
103
+ },
104
+ },
105
+ o: {
106
+ parts: {
107
+ _0: {
108
+ fields: {
109
+ amount: {
110
+ type: new TAmount('EUR'),
111
+ },
112
+ ticketNumber: {
113
+ type: new TTicketNumber(),
114
+ },
115
+ },
116
+ order: ['ticketNumber', 'amount', 'id'],
117
+ },
118
+ },
119
+ },
120
+ },
121
+ lifecycle: {
122
+ client: {
123
+ main: RegisterClientMain,
124
+ policy: EverybodyUCPolicy,
125
+ },
126
+ },
127
+ metadata: {
128
+ action: 'Create',
129
+ icon: 'user',
130
+ name: 'Register',
131
+ },
132
+ };
133
+ ```
134
+
135
+ ### Product
136
+
137
+ ```ts
138
+ const productManifest: ProductManifest = {
139
+ appReg: [{ name: 'Event' }],
140
+ name: 'Eventer',
141
+ };
142
+
143
+ const productI18n: ProductI18n = {
144
+ en: {
145
+ ...I18nEN,
146
+ ...appI18n.en,
147
+ },
148
+ fr: {
149
+ ...I18nFR,
150
+ ...appI18n.fr,
151
+ },
152
+ };
153
+ ```
154
+
155
+ ### Target
156
+
157
+ ```ts
158
+ const container = new Container(CONTAINER_OPTS);
159
+
160
+ bindCommon(container);
161
+ bindNodeCore(container);
162
+ bindProduct(container, productManifest, productI18n);
163
+ ```
164
+
165
+
14
166
  ## 👨‍💻 Contribute
15
167
 
16
168
  If you think you can help in any way, feel free to contact me (cf. `author` in `package.json`). I'd love to chat.
17
169
 
18
170
  ## ⚖️ License
19
171
 
20
- [LGPL-3.0](https://github.com/c100k/libmodulor/blob/v0.23.0/LICENSE)
172
+ [LGPL-3.0](https://github.com/c100k/libmodulor/blob/v0.25.0/LICENSE)
@@ -81,21 +81,21 @@ export const PACKAGE_JSON = (name) => `{
81
81
  "test": "tsc && vitest run --passWithNoTests"
82
82
  },
83
83
  "dependencies": {
84
- "inversify": "^7.10.4",
84
+ "inversify": "^7.10.7",
85
85
  "libmodulor": "latest",
86
86
  "reflect-metadata": "^0.2.2"
87
87
  },
88
88
  "devDependencies": {
89
- "@biomejs/biome": "^2.3.4",
90
- "@types/node": "^24.10.0",
89
+ "@biomejs/biome": "^2.3.10",
90
+ "@types/node": "^25.0.3",
91
91
  "@vitest/coverage-v8": "^3.2.4",
92
92
  "buffer": "^6.0.3",
93
93
  "cookie-parser": "^1.4.7",
94
- "express": "^5.1.0",
94
+ "express": "^5.2.1",
95
95
  "express-fileupload": "^1.5.2",
96
- "fast-check": "^4.3.0",
96
+ "fast-check": "^4.4.0",
97
97
  "helmet": "^8.1.0",
98
- "jose": "^6.1.0",
98
+ "jose": "^6.1.3",
99
99
  "typescript": "^5.9.3",
100
100
  "vite": "^6.4.1",
101
101
  "vitest": "^3.2.4"
@@ -1,8 +1,9 @@
1
1
  import type { AppName } from '../../../../app/index.js';
2
- import { type UCDef, type UCInputFieldValue } from '../../../../uc/index.js';
2
+ import { type UCDef, type UCInputFieldValue, type UCName } from '../../../../uc/index.js';
3
3
  import { type AppInput } from '../lib/app.js';
4
4
  export interface TestAppInput extends AppInput {
5
5
  appName: UCInputFieldValue<AppName>;
6
+ only: UCInputFieldValue<UCName>;
6
7
  skipCoverage: UCInputFieldValue<boolean>;
7
8
  updateSnapshots: UCInputFieldValue<boolean>;
8
9
  }
@@ -34,6 +34,7 @@ let TestAppClientMain = class TestAppClientMain {
34
34
  async exec({ uc }) {
35
35
  const appsPath = uc.reqVal0('appsPath');
36
36
  const appName = uc.reqVal0('appName');
37
+ const only = uc.rVal0('only');
37
38
  const skipCoverage = uc.reqVal0('skipCoverage');
38
39
  const updateSnapshots = uc.reqVal0('updateSnapshots');
39
40
  const appPath = this.fsManager.path(appsPath, appName);
@@ -42,6 +43,7 @@ let TestAppClientMain = class TestAppClientMain {
42
43
  }
43
44
  await this.appTestSuiteRunner.exec({
44
45
  appPath,
46
+ only,
45
47
  skipCoverage,
46
48
  updateSnapshots,
47
49
  });
@@ -84,6 +86,12 @@ export const TestAppUCD = {
84
86
  appName: {
85
87
  type: new TString().setExamples([APP_NAME_PLACEHOLDER]),
86
88
  },
89
+ only: {
90
+ cardinality: {
91
+ min: 0,
92
+ },
93
+ type: new TString().setExamples(['CreatePost']),
94
+ },
87
95
  skipCoverage: {
88
96
  type: new TBoolean().setDefaultValue(false),
89
97
  },
@@ -36,6 +36,9 @@ export declare const UC_DEF_TYPE: string;
36
36
  export declare const UC_INPUT_BASE: string;
37
37
  export declare const UC_INPUT_FIELD_PATTERN: string;
38
38
  export declare const UC_INPUT_SUFFIX: string;
39
+ export declare const UC_LIFECYCLE_PROP_NAME: string;
40
+ export declare const UC_LIFECYCLE_CLIENT_PROP_NAME: string;
41
+ export declare const UC_LIFECYCLE_SERVER_PROP_NAME: string;
39
42
  export declare const UC_MAIN_SUFFIX: string;
40
43
  export declare const UC_MAIN_CLIENT_SUFFIX: string;
41
44
  export declare const UC_MAIN_SERVER_SUFFIX: string;
@@ -46,6 +46,9 @@ export const UC_DEF_TYPE = 'UCDef';
46
46
  export const UC_INPUT_BASE = 'UCInput';
47
47
  export const UC_INPUT_FIELD_PATTERN = '^UCInputFieldValue<(.*)>$';
48
48
  export const UC_INPUT_SUFFIX = 'Input';
49
+ export const UC_LIFECYCLE_PROP_NAME = 'lifecycle';
50
+ export const UC_LIFECYCLE_CLIENT_PROP_NAME = 'client';
51
+ export const UC_LIFECYCLE_SERVER_PROP_NAME = 'server';
49
52
  export const UC_MAIN_SUFFIX = 'Main';
50
53
  export const UC_MAIN_CLIENT_SUFFIX = `Client${UC_MAIN_SUFFIX}`;
51
54
  export const UC_MAIN_SERVER_SUFFIX = `Server${UC_MAIN_SUFFIX}`;
@@ -1,2 +1 @@
1
- import type { DataType } from './DataType.js';
2
- export declare const DataTypes: DataType[];
1
+ export declare const DataTypes: readonly ["Address", "Amount", "ApiKey", "BarCode", "CSS", "Color", "ColorRGBA", "CompanyName", "CountryISO3166Alpha2", "CurrencyISO4217", "DateISO8601", "DateTimeFormat", "DirPath", "DomainName", "Email", "EmbeddedObject", "Emoji", "EncryptionKey", "ErrorMessage", "ExternalServiceId", "File", "FileExtension", "FileMimeType", "FileName", "FilePath", "FreeTextLong", "FreeTextShort", "Geolocation", "GitSSHURL", "HTML", "HTTPContentType", "HTTPMethod", "HTTPStatusNumber", "HostAddress", "HostPort", "IPv4", "IPv6", "JSONString", "JWT", "JavaScript", "JobTitle", "Markdown", "NumIndex", "Password", "Percentage", "PersonFirstname", "PersonFullname", "PersonInitials", "PersonLastname", "QRCode", "SQLQuery", "SSHPrivateKey", "SSHPublicKey", "SearchQuery", "SemVerVersion", "ShellCommand", "Slug", "Time", "Timestamp", "TransportType", "UIntDuration", "UIntQuantity", "URL", "URLPath", "UUID", "Username", "Year", "YesNo"];
@@ -1,13 +1,13 @@
1
1
  import type { I18nTranslation } from '../i18n/index.js';
2
2
  import type { NumIndex } from './final/TNumIndex.js';
3
- type ViolationConstraint = 'fieldsOr' | 'format' | 'mandatory' | 'max' | 'maxCount' | 'maxLength' | 'min' | 'minCount' | 'minLength' | 'oneOf' | 'shape' | 'type';
3
+ type ViolationConstraint = 'fieldsOr' | 'format' | 'mandatory' | 'max' | 'maxCount' | 'maxLength' | 'maxSize' | 'min' | 'minCount' | 'minLength' | 'minSize' | 'oneOf' | 'shape' | 'type';
4
4
  type ViolationBase<C extends ViolationConstraint, EV> = {
5
5
  constraint: C;
6
6
  expected: EV;
7
7
  };
8
8
  export type ViolationFormat = 'ColorRGBA' | 'DateISO8601' | 'DirPath' | 'DomainName' | 'Email' | 'FilePath' | 'GitSSHURL' | 'IPv4' | 'IPv6' | 'JSON' | 'JWT' | 'PersonFirstname' | 'PersonFullname' | 'PersonInitials' | 'PersonLastname' | 'QRCode' | 'SemVerVersion' | 'Slug' | 'SSHPrivateKey' | 'SSHPublicKey' | 'Time' | 'URL' | 'UUID';
9
9
  export type ViolationType = 'array' | 'boolean' | 'int' | 'number' | 'object' | 'scalar' | 'string';
10
- export type Violation<T = unknown> = ViolationBase<'fieldsOr', string> | ViolationBase<'format', ViolationFormat> | ViolationBase<'format', string> | ViolationBase<'mandatory', undefined> | ViolationBase<'max', number> | ViolationBase<'maxCount', number> | ViolationBase<'maxLength', number> | ViolationBase<'min', number> | ViolationBase<'minCount', number> | ViolationBase<'minLength', number> | ViolationBase<'oneOf', T[]> | ViolationBase<'shape', object> | ViolationBase<'type', ViolationType>;
10
+ export type Violation<T = unknown> = ViolationBase<'fieldsOr', string> | ViolationBase<'format', ViolationFormat> | ViolationBase<'format', string> | ViolationBase<'mandatory', undefined> | ViolationBase<'max', number> | ViolationBase<'maxCount', number> | ViolationBase<'maxLength', number> | ViolationBase<'maxSize', string> | ViolationBase<'min', number> | ViolationBase<'minCount', number> | ViolationBase<'minLength', number> | ViolationBase<'minSize', string> | ViolationBase<'oneOf', T[]> | ViolationBase<'shape', object> | ViolationBase<'type', ViolationType>;
11
11
  type ViolationI18nableSimple = Exclude<ViolationConstraint, 'format' | 'type'>;
12
12
  export type ViolationI18nable = `validation_${ViolationI18nableSimple}` | `validation_format_${ViolationFormat}` | `validation_type_${ViolationType}`;
13
13
  export type ViolationI18n = Record<ViolationI18nable, I18nTranslation>;
@@ -26,6 +26,7 @@ export interface OptionsOpts {
26
26
  shouldTranslateLabels?: boolean;
27
27
  strict?: boolean;
28
28
  }
29
+ export type ConstraintsForHuman = Record<string, string>;
29
30
  export declare abstract class TBase<T extends DataType> {
30
31
  static DEFAULT_OPTIONS: Required<OptionsOpts>;
31
32
  protected defaultValue: T | undefined;
@@ -44,6 +45,7 @@ export declare abstract class TBase<T extends DataType> {
44
45
  fmt(ifNullOrUndefined?: string): string;
45
46
  getDefaultValue(): T | undefined;
46
47
  getExamples(): T[] | undefined;
48
+ getConstraintsForHuman(): ConstraintsForHuman | null;
47
49
  getInitialValue(): T | undefined;
48
50
  getOptions(): Option<T>[] | undefined;
49
51
  getSemanticsMapping(): SemanticsMapping | undefined;
@@ -39,6 +39,9 @@ export class TBase {
39
39
  getExamples() {
40
40
  return this.examples;
41
41
  }
42
+ getConstraintsForHuman() {
43
+ return null;
44
+ }
42
45
  getInitialValue() {
43
46
  return this.initialValue;
44
47
  }
@@ -1,6 +1,7 @@
1
+ import type { EnumOf } from '../../utils/index.js';
1
2
  import type { Validation } from '../Validation.js';
2
3
  import { TBase, type TName } from './TBase.js';
3
- export declare enum TObjectShapeValidationStrategy {
4
+ export declare const TObjectShapeValidationStrategy: {
4
5
  /**
5
6
  * No shape validation is performed
6
7
  *
@@ -8,14 +9,15 @@ export declare enum TObjectShapeValidationStrategy {
8
9
  *
9
10
  * Otherwise, you can still override {@link validate} in the `T*` class and do your own validation.
10
11
  */
11
- NONE = "NONE",
12
+ readonly NONE: "NONE";
12
13
  /**
13
14
  * Validate against the {@link TObject.example()}
14
15
  *
15
16
  * It checks that the keys of the value, sorted alphabetically, are the same as the example's keys.
16
17
  */
17
- SAME_AS_EXAMPLE = "SAME_AS_EXAMPLE"
18
- }
18
+ readonly SAME_AS_EXAMPLE: "SAME_AS_EXAMPLE";
19
+ };
20
+ export type TObjectShapeValidationStrategy = EnumOf<typeof TObjectShapeValidationStrategy>;
19
21
  export interface TObjectConstraints {
20
22
  /**
21
23
  * @defaultValue {@link TObjectShapeValidationStrategy.SAME_AS_EXAMPLE}
@@ -1,6 +1,5 @@
1
1
  import { TBase } from './TBase.js';
2
- export var TObjectShapeValidationStrategy;
3
- (function (TObjectShapeValidationStrategy) {
2
+ export const TObjectShapeValidationStrategy = {
4
3
  /**
5
4
  * No shape validation is performed
6
5
  *
@@ -8,14 +7,14 @@ export var TObjectShapeValidationStrategy;
8
7
  *
9
8
  * Otherwise, you can still override {@link validate} in the `T*` class and do your own validation.
10
9
  */
11
- TObjectShapeValidationStrategy["NONE"] = "NONE";
10
+ NONE: 'NONE',
12
11
  /**
13
12
  * Validate against the {@link TObject.example()}
14
13
  *
15
14
  * It checks that the keys of the value, sorted alphabetically, are the same as the example's keys.
16
15
  */
17
- TObjectShapeValidationStrategy["SAME_AS_EXAMPLE"] = "SAME_AS_EXAMPLE";
18
- })(TObjectShapeValidationStrategy || (TObjectShapeValidationStrategy = {}));
16
+ SAME_AS_EXAMPLE: 'SAME_AS_EXAMPLE',
17
+ };
19
18
  export class TObject extends TBase {
20
19
  constraints;
21
20
  constructor(constraints = {
@@ -65,7 +64,7 @@ export class TObject extends TBase {
65
64
  break;
66
65
  }
67
66
  default:
68
- ((_) => { })(strategy);
67
+ strategy;
69
68
  }
70
69
  }
71
70
  return validation;
@@ -1,5 +1,5 @@
1
1
  import type { Validation, ViolationFormat } from '../Validation.js';
2
- import { TBase, type TName } from './TBase.js';
2
+ import { type ConstraintsForHuman, TBase, type TName } from './TBase.js';
3
3
  export interface TStringConstraints<VF extends ViolationFormat | (string & {}) = ViolationFormat> {
4
4
  format?: {
5
5
  f: VF;
@@ -15,6 +15,7 @@ export declare class TString<T extends string = string, VF extends ViolationForm
15
15
  constructor(constraints?: TStringConstraints<VF> | undefined);
16
16
  tName(): TName;
17
17
  example(): T;
18
+ getConstraintsForHuman(): ConstraintsForHuman | null;
18
19
  getConstraints(): TStringConstraints<VF> | undefined;
19
20
  isPotentiallyLong(): boolean;
20
21
  protected removeFormatConstraint(): void;
@@ -13,6 +13,28 @@ export class TString extends TBase {
13
13
  example() {
14
14
  return 'Miami';
15
15
  }
16
+ getConstraintsForHuman() {
17
+ if (!this.constraints) {
18
+ return null;
19
+ }
20
+ const c = {};
21
+ const { format, maxLength, minLength } = this.constraints;
22
+ if (minLength) {
23
+ // biome-ignore lint/complexity/useLiteralKeys: typescript disagrees
24
+ c['minLength'] = minLength.toString();
25
+ }
26
+ if (maxLength) {
27
+ // biome-ignore lint/complexity/useLiteralKeys: typescript disagrees
28
+ c['maxLength'] = maxLength.toString();
29
+ }
30
+ if (format) {
31
+ // Are regexes really "human" ?
32
+ // TODO : Find a more human way of displaying regexes
33
+ // biome-ignore lint/complexity/useLiteralKeys: typescript disagrees
34
+ c['format'] = format.regexp.toString();
35
+ }
36
+ return c;
37
+ }
16
38
  getConstraints() {
17
39
  return this.constraints;
18
40
  }
@@ -1,23 +1,32 @@
1
- import type { TName } from '../base/TBase.js';
1
+ import type { ConstraintsForHuman, TName } from '../base/TBase.js';
2
2
  import { TObject } from '../base/TObject.js';
3
3
  import type { HTMLInputType } from '../targets/web.js';
4
4
  import type { Validation } from '../Validation.js';
5
- import { type FileMimeType, type TFileMimeTypeConstraints } from './TFileMimeType.js';
5
+ import { type FileMimeType } from './TFileMimeType.js';
6
6
  import { type FileName } from './TFileName.js';
7
7
  import { type FilePath } from './TFilePath.js';
8
+ import type { UIntQuantity } from './TUIntQuantity.js';
8
9
  export type File = {
9
10
  name: FileName;
10
- path: FilePath;
11
+ size: UIntQuantity;
11
12
  type: FileMimeType;
13
+ uri: FilePath;
12
14
  };
13
15
  export interface TFileConstraints {
14
- type: TFileMimeTypeConstraints;
16
+ accept: FileMimeType[];
17
+ maxSizeInBytes?: UIntQuantity;
18
+ minSizeInBytes?: UIntQuantity;
15
19
  }
16
20
  export declare class TFile extends TObject<File> {
17
21
  protected fileConstraints: TFileConstraints;
22
+ static readonly UNITS: string[];
18
23
  constructor(fileConstraints: TFileConstraints);
19
24
  tName(): TName;
20
25
  example(): File;
26
+ getConstraintsForHuman(): ConstraintsForHuman | null;
21
27
  htmlInputType(): HTMLInputType;
22
28
  validate(): Validation;
29
+ getFileConstraints(): TFileConstraints;
30
+ fmtBytes(bytes: number, decimals?: number): string;
31
+ withOneExample(name: File['name']): this;
23
32
  }
@@ -1,9 +1,21 @@
1
1
  import { TObject, TObjectShapeValidationStrategy } from '../base/TObject.js';
2
- import { TFileMimeType, } from './TFileMimeType.js';
2
+ import { TFileMimeType } from './TFileMimeType.js';
3
3
  import { TFileName } from './TFileName.js';
4
4
  import { TFilePath } from './TFilePath.js';
5
5
  export class TFile extends TObject {
6
6
  fileConstraints;
7
+ // We consider them generic enough to not having to be translated
8
+ static UNITS = [
9
+ 'B',
10
+ 'KB',
11
+ 'MB',
12
+ 'GB',
13
+ 'TB',
14
+ 'PB',
15
+ 'EB',
16
+ 'ZB',
17
+ 'YB',
18
+ ];
7
19
  constructor(fileConstraints) {
8
20
  super({
9
21
  // We usually process instances of https://developer.mozilla.org/fr/docs/Web/API/File
@@ -17,11 +29,27 @@ export class TFile extends TObject {
17
29
  }
18
30
  example() {
19
31
  return {
20
- name: 'picture.png',
21
- path: '/Users/dexter/Desktop/picture.png',
22
- type: 'image/png',
32
+ name: TFilePath.FILE_NAME,
33
+ size: TFilePath.FILE_SIZE,
34
+ type: TFilePath.MIME_TYPE,
35
+ uri: `${TFilePath.ABS_PATH}/${TFilePath.FILE_NAME}`,
23
36
  };
24
37
  }
38
+ getConstraintsForHuman() {
39
+ const c = {};
40
+ const { accept, maxSizeInBytes, minSizeInBytes } = this.fileConstraints;
41
+ if (minSizeInBytes) {
42
+ // biome-ignore lint/complexity/useLiteralKeys: typescript disagrees
43
+ c['minSizeInBytes'] = this.fmtBytes(minSizeInBytes);
44
+ }
45
+ if (maxSizeInBytes) {
46
+ // biome-ignore lint/complexity/useLiteralKeys: typescript disagrees
47
+ c['maxSizeInBytes'] = this.fmtBytes(maxSizeInBytes);
48
+ }
49
+ // biome-ignore lint/complexity/useLiteralKeys: typescript disagrees
50
+ c['accept'] = accept.join(', ');
51
+ return c;
52
+ }
25
53
  htmlInputType() {
26
54
  return 'file';
27
55
  }
@@ -32,13 +60,47 @@ export class TFile extends TObject {
32
60
  }
33
61
  const val = this.raw;
34
62
  validation.concat(new TFileName().assign(val.name).validate());
63
+ const { accept, maxSizeInBytes, minSizeInBytes } = this.fileConstraints;
64
+ const { size, type, uri } = val;
35
65
  if (!(val instanceof File)) {
36
- validation.concat(new TFilePath().assign(val.path).validate());
66
+ validation.concat(new TFilePath().assign(uri).validate());
37
67
  }
38
- validation.concat(new TFileMimeType(this.fileConstraints.type)
39
- .assign(val.type)
68
+ validation.concat(new TFileMimeType()
69
+ .setOptions(accept.map((a) => ({ label: a, value: a })))
70
+ .assign(type)
40
71
  .validate());
41
- // TODO : Add validation on file size
72
+ if (minSizeInBytes && size < minSizeInBytes) {
73
+ validation.add({
74
+ constraint: 'minSize',
75
+ expected: this.fmtBytes(minSizeInBytes),
76
+ });
77
+ }
78
+ if (maxSizeInBytes && size > maxSizeInBytes) {
79
+ validation.add({
80
+ constraint: 'maxSize',
81
+ expected: this.fmtBytes(maxSizeInBytes),
82
+ });
83
+ }
42
84
  return validation;
43
85
  }
86
+ getFileConstraints() {
87
+ return this.fileConstraints;
88
+ }
89
+ fmtBytes(bytes, decimals = 2) {
90
+ const k = 1024;
91
+ const dm = decimals < 0 ? 0 : decimals;
92
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
93
+ return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${TFile.UNITS[i]}`;
94
+ }
95
+ withOneExample(name) {
96
+ this.setExamples([
97
+ {
98
+ name,
99
+ size: TFilePath.FILE_SIZE,
100
+ type: this.fileConstraints.accept[0] ?? TFilePath.MIME_TYPE,
101
+ uri: `${TFilePath.ABS_PATH}/${name}`,
102
+ },
103
+ ]);
104
+ return this;
105
+ }
44
106
  }
@@ -3,7 +3,7 @@ import { TString, type TStringConstraints } from '../base/TString.js';
3
3
  type ImageExtension = 'gif' | 'heic' | 'jpg' | 'png';
4
4
  type VideoExtension = 'mov' | 'mp4';
5
5
  type XMLExtension = 'gpx' | 'xml';
6
- export type FileExtension = ImageExtension | VideoExtension | XMLExtension;
6
+ export type FileExtension = ImageExtension | VideoExtension | XMLExtension | (string & {});
7
7
  export interface TFileExtensionConstraints extends TStringConstraints {
8
8
  allowed: FileExtension[];
9
9
  }
@@ -1,11 +1,7 @@
1
1
  import type { TName } from '../base/TBase.js';
2
- import { TString, type TStringConstraints } from '../base/TString.js';
3
- export type FileMimeType = 'text/plain' | 'audio/aac' | 'application/x-abiword' | 'application/octet-stream' | 'video/x-msvideo' | 'application/vnd.amazon.ebook' | 'image/bmp' | 'application/x-bzip' | 'application/x-bzip2' | 'application/x-csh' | 'text/css' | 'text/csv' | 'application/msword' | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' | 'application/vnd.ms-fontobject' | 'application/epub+zip' | 'image/gif' | 'text/html' | 'image/x-icon' | 'text/calendar' | 'application/java-archive' | 'image/jpeg' | 'image/jpg' | 'application/javascript' | 'application/json' | 'audio/midi' | 'video/mpeg' | 'application/vnd.apple.installer+xml' | 'application/vnd.oasis.opendocument.presentation' | 'application/vnd.oasis.opendocument.spreadsheet' | 'application/vnd.oasis.opendocument.text' | 'audio/ogg' | 'video/ogg' | 'application/ogg' | 'font/otf' | 'image/png' | 'application/pdf' | 'application/vnd.ms-powerpoint' | 'application/vnd.openxmlformats-officedocument.presentationml.presentation' | 'application/x-rar-compressed' | 'application/rtf' | 'application/x-sh' | 'image/svg+xml' | 'application/x-shockwave-flash' | 'application/x-tar' | 'image/tiff' | 'application/typescript' | 'font/ttf' | 'application/vnd.visio' | 'audio/x-wav' | 'audio/webm' | 'video/webm' | 'image/webp' | 'font/woff' | 'font/woff2' | 'application/xhtml+xml' | 'application/vnd.ms-excel' | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' | 'application/xml' | 'application/vnd.mozilla.xul+xml' | 'application/zip' | 'video/3gpp audio/3gpp' | 'video/3gpp2 audio/3gpp2' | 'application/x-7z-compressed';
4
- export interface TFileMimeTypeConstraints extends TStringConstraints {
5
- allowed: FileMimeType[];
6
- }
2
+ import { TString } from '../base/TString.js';
3
+ export type FileMimeType = 'text/plain' | 'audio/aac' | 'application/x-abiword' | 'application/octet-stream' | 'video/x-msvideo' | 'application/vnd.amazon.ebook' | 'image/bmp' | 'application/x-bzip' | 'application/x-bzip2' | 'application/x-csh' | 'text/css' | 'text/csv' | 'application/msword' | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' | 'application/vnd.ms-fontobject' | 'application/epub+zip' | 'image/gif' | 'text/html' | 'image/x-icon' | 'text/calendar' | 'application/java-archive' | 'image/jpeg' | 'image/jpg' | 'application/javascript' | 'application/json' | 'audio/midi' | 'video/mpeg' | 'application/vnd.apple.installer+xml' | 'application/vnd.oasis.opendocument.presentation' | 'application/vnd.oasis.opendocument.spreadsheet' | 'application/vnd.oasis.opendocument.text' | 'audio/ogg' | 'video/ogg' | 'application/ogg' | 'font/otf' | 'image/png' | 'application/pdf' | 'application/vnd.ms-powerpoint' | 'application/vnd.openxmlformats-officedocument.presentationml.presentation' | 'application/x-rar-compressed' | 'application/rtf' | 'application/x-sh' | 'image/svg+xml' | 'application/x-shockwave-flash' | 'application/x-tar' | 'image/tiff' | 'application/typescript' | 'font/ttf' | 'application/vnd.visio' | 'audio/x-wav' | 'audio/webm' | 'video/webm' | 'image/webp' | 'font/woff' | 'font/woff2' | 'application/xhtml+xml' | 'application/vnd.ms-excel' | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' | 'application/xml' | 'application/vnd.mozilla.xul+xml' | 'application/zip' | 'video/3gpp audio/3gpp' | 'video/3gpp2 audio/3gpp2' | 'application/x-7z-compressed' | (string & {});
7
4
  export declare class TFileMimeType extends TString<FileMimeType> {
8
- constructor(constraints?: TFileMimeTypeConstraints);
9
5
  tName(): TName;
10
6
  example(): FileMimeType;
11
7
  }
@@ -1,11 +1,5 @@
1
1
  import { TString } from '../base/TString.js';
2
2
  export class TFileMimeType extends TString {
3
- constructor(constraints) {
4
- super(constraints);
5
- if (constraints?.allowed) {
6
- this.setOptions(constraints?.allowed.map((v) => ({ label: v, value: v })));
7
- }
8
- }
9
3
  tName() {
10
4
  return 'FileMimeType';
11
5
  }