alepha 0.19.4 → 0.19.5
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/api/audits/index.d.ts +8 -8
- package/dist/api/issues/index.d.ts +810 -0
- package/dist/api/issues/index.d.ts.map +1 -0
- package/dist/api/issues/index.js +447 -0
- package/dist/api/issues/index.js.map +1 -0
- package/dist/api/keys/index.d.ts +5 -5
- package/dist/api/users/index.d.ts +6 -0
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +10 -3
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/workflows/index.d.ts +3 -3
- package/dist/captcha/index.d.ts +142 -0
- package/dist/captcha/index.d.ts.map +1 -0
- package/dist/captcha/index.js +177 -0
- package/dist/captcha/index.js.map +1 -0
- package/dist/cli/core/index.d.ts +82 -2
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +90 -6
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +84 -10
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +92 -4
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +30 -3
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js +98 -21
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +2 -3
- package/dist/command/index.js.map +1 -1
- package/dist/orm/core/index.bun.js +6 -6
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +6 -6
- package/dist/orm/core/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +1 -0
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/i18n/index.js +8 -4
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +145 -2
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +364 -63
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js.map +1 -1
- package/package.json +11 -1
- package/src/api/issues/__tests__/IssueService.spec.ts +263 -0
- package/src/api/issues/controllers/AdminIssueController.ts +149 -0
- package/src/api/issues/controllers/IssueController.ts +44 -0
- package/src/api/issues/entities/issues.ts +49 -0
- package/src/api/issues/index.ts +53 -0
- package/src/api/issues/schemas/createIssueSchema.ts +13 -0
- package/src/api/issues/schemas/issueConfigAtom.ts +13 -0
- package/src/api/issues/schemas/issueQuerySchema.ts +18 -0
- package/src/api/issues/schemas/issueResourceSchema.ts +6 -0
- package/src/api/issues/schemas/myIssueQuerySchema.ts +10 -0
- package/src/api/issues/schemas/updateIssueSchema.ts +13 -0
- package/src/api/issues/services/IssueService.ts +264 -0
- package/src/api/users/primitives/$realm.ts +24 -0
- package/src/api/users/services/CredentialService.ts +6 -3
- package/src/api/users/services/RegistrationService.ts +15 -5
- package/src/api/users/services/SessionService.ts +2 -0
- package/src/captcha/__tests__/MemoryCaptchaProvider.spec.ts +74 -0
- package/src/captcha/index.ts +33 -0
- package/src/captcha/providers/CaptchaProvider.ts +17 -0
- package/src/captcha/providers/MemoryCaptchaProvider.ts +65 -0
- package/src/captcha/providers/TurnstileCaptchaProvider.ts +125 -0
- package/src/cli/core/atoms/buildOptions.ts +57 -0
- package/src/cli/core/commands/build.ts +2 -0
- package/src/cli/core/providers/ViteDevServerProvider.ts +1 -1
- package/src/cli/core/services/ViteUtils.ts +5 -2
- package/src/cli/core/tasks/BuildClientTask.ts +3 -1
- package/src/cli/core/tasks/BuildCloudflareTask.ts +4 -0
- package/src/cli/core/tasks/BuildPwaTask.ts +81 -0
- package/src/cli/platform/adapters/CloudflareAdapter.ts +24 -0
- package/src/cli/platform/atoms/platformOptions.ts +19 -3
- package/src/cli/platform/hooks/PlatformHook.ts +51 -0
- package/src/cli/platform/index.ts +1 -0
- package/src/cli/platform/services/CloudflareApi.ts +22 -1
- package/src/cli/platform/services/PlatformOrchestrator.ts +67 -2
- package/src/cli/vendor/__tests__/VendorService.spec.ts +40 -1
- package/src/cli/vendor/commands/VendorCommand.ts +41 -38
- package/src/cli/vendor/services/VendorService.ts +108 -4
- package/src/command/__tests__/CliProvider.spec.ts +45 -0
- package/src/command/providers/CliProvider.ts +3 -4
- package/src/orm/core/services/Repository.ts +20 -6
- package/src/react/i18n/__tests__/I18nProvider.spec.ts +83 -0
- package/src/react/i18n/providers/I18nProvider.ts +12 -10
- package/src/security/primitives/$issuer.ts +3 -1
- package/src/server/auth/index.ts +7 -0
- package/src/server/auth/primitives/$auth.ts +37 -3
- package/src/server/auth/primitives/$authApple.ts +114 -4
- package/src/server/auth/primitives/$authFacebook.ts +98 -0
- package/src/server/auth/primitives/$authFranceConnect.ts +105 -0
- package/src/server/auth/primitives/$authGithub.ts +22 -16
- package/src/server/auth/primitives/$authMicrosoft.ts +88 -0
- package/src/server/auth/providers/ServerAuthProvider.ts +197 -72
- package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -0
- package/src/server/core/__tests__/ServerRouterProvider-errorHandler.spec.ts +1 -1
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +3 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $inject, AlephaError } from "alepha";
|
|
1
|
+
import { $inject, Alepha, AlephaError } from "alepha";
|
|
2
2
|
import type { RunnerMethod } from "alepha/command";
|
|
3
3
|
import { $logger, ConsoleColorProvider } from "alepha/logger";
|
|
4
4
|
import { CloudflareAdapter } from "../adapters/CloudflareAdapter.ts";
|
|
@@ -9,6 +9,10 @@ import type {
|
|
|
9
9
|
PlatformState,
|
|
10
10
|
} from "../adapters/PlatformAdapter.ts";
|
|
11
11
|
import { VercelAdapter } from "../adapters/VercelAdapter.ts";
|
|
12
|
+
import {
|
|
13
|
+
PlatformHook,
|
|
14
|
+
type PlatformHookContext,
|
|
15
|
+
} from "../hooks/PlatformHook.ts";
|
|
12
16
|
import { type NamingContext, NamingService } from "./NamingService.ts";
|
|
13
17
|
import {
|
|
14
18
|
PlatformInspector,
|
|
@@ -28,6 +32,7 @@ export class PlatformOrchestrator {
|
|
|
28
32
|
protected readonly naming = $inject(NamingService);
|
|
29
33
|
protected readonly cloudflareAdapter = $inject(CloudflareAdapter);
|
|
30
34
|
protected readonly vercelAdapter = $inject(VercelAdapter);
|
|
35
|
+
protected readonly alepha = $inject(Alepha);
|
|
31
36
|
|
|
32
37
|
// -------------------------------------------------------------------------
|
|
33
38
|
// Adapter resolution
|
|
@@ -104,7 +109,12 @@ export class PlatformOrchestrator {
|
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
111
|
|
|
107
|
-
// 7.
|
|
112
|
+
// 7. Platform hooks (register external resources: Stripe webhooks, etc.)
|
|
113
|
+
// Run before secrets() so any secret a hook writes to .env.<env>
|
|
114
|
+
// gets pushed to the deployed worker in the same up cycle.
|
|
115
|
+
await this.runHooks("register", ctx, urls, run);
|
|
116
|
+
|
|
117
|
+
// 8. Secrets (push .env.{env} secrets to deployed workers)
|
|
108
118
|
await adapter.secrets(ctx, run);
|
|
109
119
|
|
|
110
120
|
run.end();
|
|
@@ -166,6 +176,9 @@ export class PlatformOrchestrator {
|
|
|
166
176
|
// Auth
|
|
167
177
|
await adapter.authenticate(ctx, run);
|
|
168
178
|
|
|
179
|
+
// Platform hooks (tear down external resources first, while creds still valid)
|
|
180
|
+
await this.runHooks("unregister", ctx, [], run);
|
|
181
|
+
|
|
169
182
|
// Teardown
|
|
170
183
|
await adapter.teardown(ctx, run);
|
|
171
184
|
run.end();
|
|
@@ -173,6 +186,58 @@ export class PlatformOrchestrator {
|
|
|
173
186
|
return true;
|
|
174
187
|
}
|
|
175
188
|
|
|
189
|
+
// -------------------------------------------------------------------------
|
|
190
|
+
// Platform hooks
|
|
191
|
+
// -------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Run all registered PlatformHook instances.
|
|
195
|
+
*
|
|
196
|
+
* Discovered dynamically via `alepha.services(PlatformHook)`: any plugin
|
|
197
|
+
* that registers a PlatformHook subclass in its `$module.services`
|
|
198
|
+
* participates automatically, without the core knowing about it.
|
|
199
|
+
*/
|
|
200
|
+
protected async runHooks(
|
|
201
|
+
phase: "register" | "unregister",
|
|
202
|
+
ctx: PlatformContext,
|
|
203
|
+
deployUrls: string[],
|
|
204
|
+
run: RunnerMethod,
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
const hooks = this.alepha.services(PlatformHook);
|
|
207
|
+
if (hooks.length === 0) return;
|
|
208
|
+
|
|
209
|
+
const baseUrl = ctx.envConfig.domain
|
|
210
|
+
? `https://${ctx.envConfig.domain}`
|
|
211
|
+
: deployUrls[0];
|
|
212
|
+
|
|
213
|
+
if (!baseUrl) {
|
|
214
|
+
this.log.debug("Skipping platform hooks: no base URL available");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const hookCtx: PlatformHookContext = { ...ctx, baseUrl, run };
|
|
219
|
+
|
|
220
|
+
for (const hook of hooks) {
|
|
221
|
+
this.log.info(`Platform hook: ${hook.name} (${phase})`);
|
|
222
|
+
try {
|
|
223
|
+
if (phase === "register") {
|
|
224
|
+
await hook.register(hookCtx);
|
|
225
|
+
} else {
|
|
226
|
+
await hook.unregister(hookCtx);
|
|
227
|
+
}
|
|
228
|
+
} catch (err) {
|
|
229
|
+
// unregister must never block teardown
|
|
230
|
+
if (phase === "unregister") {
|
|
231
|
+
this.log.warn(
|
|
232
|
+
`Platform hook ${hook.name} failed to unregister: ${(err as Error).message}`,
|
|
233
|
+
);
|
|
234
|
+
} else {
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
176
241
|
// -------------------------------------------------------------------------
|
|
177
242
|
// plan
|
|
178
243
|
// -------------------------------------------------------------------------
|
|
@@ -443,7 +443,12 @@ describe("VendorService", () => {
|
|
|
443
443
|
packages: ["pkg"],
|
|
444
444
|
});
|
|
445
445
|
|
|
446
|
-
expect(result.packages[0].modified).
|
|
446
|
+
expect(result.packages[0].modified).toHaveLength(1);
|
|
447
|
+
expect(result.packages[0].modified[0].file).toBe("file.ts");
|
|
448
|
+
expect(result.packages[0].modified[0].changes).toEqual([
|
|
449
|
+
{ line: 1, type: "removed", text: "original" },
|
|
450
|
+
{ line: 1, type: "added", text: "modified by user" },
|
|
451
|
+
]);
|
|
447
452
|
expect(result.totalChanges).toBe(1);
|
|
448
453
|
});
|
|
449
454
|
|
|
@@ -493,6 +498,40 @@ describe("VendorService", () => {
|
|
|
493
498
|
expect(result.totalChanges).toBe(0);
|
|
494
499
|
});
|
|
495
500
|
|
|
501
|
+
it("should include line-level changes for multi-line modifications", async ({
|
|
502
|
+
expect,
|
|
503
|
+
}) => {
|
|
504
|
+
const { service, fs } = createDiffTestService();
|
|
505
|
+
await writeVendorLock(fs);
|
|
506
|
+
|
|
507
|
+
await fs.mkdir("/tmp/test-baseline/packages/pkg", { recursive: true });
|
|
508
|
+
await fs.writeFile(
|
|
509
|
+
"/tmp/test-baseline/packages/pkg/config.ts",
|
|
510
|
+
"const a = 1;\nconst b = 2;\nconst c = 3;",
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
await fs.mkdir("/project/packages/pkg", { recursive: true });
|
|
514
|
+
await fs.writeFile(
|
|
515
|
+
"/project/packages/pkg/config.ts",
|
|
516
|
+
"const a = 1;\nconst b = 99;\nconst c = 3;\nconst d = 4;",
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const result = await service.diff({
|
|
520
|
+
root: "/project",
|
|
521
|
+
remote: "remote",
|
|
522
|
+
branch: "main",
|
|
523
|
+
packages: ["pkg"],
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const fileDiff = result.packages[0].modified[0];
|
|
527
|
+
expect(fileDiff.file).toBe("config.ts");
|
|
528
|
+
expect(fileDiff.changes).toEqual([
|
|
529
|
+
{ line: 2, type: "removed", text: "const b = 2;" },
|
|
530
|
+
{ line: 2, type: "added", text: "const b = 99;" },
|
|
531
|
+
{ line: 4, type: "added", text: "const d = 4;" },
|
|
532
|
+
]);
|
|
533
|
+
});
|
|
534
|
+
|
|
496
535
|
it("should clean up temp directory after diff", async ({ expect }) => {
|
|
497
536
|
const { service, fs } = createDiffTestService();
|
|
498
537
|
await writeVendorLock(fs);
|
|
@@ -5,6 +5,7 @@ import { $logger, ConsoleColorProvider } from "alepha/logger";
|
|
|
5
5
|
import { vendorOptions } from "../atoms/vendorOptions.ts";
|
|
6
6
|
import type {
|
|
7
7
|
VendorDiffResult,
|
|
8
|
+
VendorPackageDiff,
|
|
8
9
|
VendorSyncResult,
|
|
9
10
|
} from "../services/VendorService.ts";
|
|
10
11
|
import { VendorService } from "../services/VendorService.ts";
|
|
@@ -81,22 +82,7 @@ export class VendorCommand {
|
|
|
81
82
|
);
|
|
82
83
|
|
|
83
84
|
for (const pkg of result.aborted.packages) {
|
|
84
|
-
|
|
85
|
-
pkg.added.length + pkg.modified.length + pkg.removed.length;
|
|
86
|
-
if (count === 0) continue;
|
|
87
|
-
|
|
88
|
-
process.stdout.write(
|
|
89
|
-
`\n${c.set("CYAN", pkg.name)}: ${count} ${count === 1 ? "file differs" : "files differ"}\n`,
|
|
90
|
-
);
|
|
91
|
-
for (const file of pkg.added) {
|
|
92
|
-
process.stdout.write(` ${c.set("GREEN", "A")} ${file}\n`);
|
|
93
|
-
}
|
|
94
|
-
for (const file of pkg.modified) {
|
|
95
|
-
process.stdout.write(` ${c.set("ORANGE", "M")} ${file}\n`);
|
|
96
|
-
}
|
|
97
|
-
for (const file of pkg.removed) {
|
|
98
|
-
process.stdout.write(` ${c.set("RED", "D")} ${file}\n`);
|
|
99
|
-
}
|
|
85
|
+
this.printPackageDiff(pkg);
|
|
100
86
|
}
|
|
101
87
|
|
|
102
88
|
process.stdout.write("\n");
|
|
@@ -160,35 +146,52 @@ export class VendorCommand {
|
|
|
160
146
|
return;
|
|
161
147
|
}
|
|
162
148
|
|
|
163
|
-
const c = this.color;
|
|
164
|
-
|
|
165
149
|
for (const pkg of result.packages) {
|
|
166
|
-
|
|
167
|
-
pkg.added.length + pkg.modified.length + pkg.removed.length;
|
|
168
|
-
if (count === 0) {
|
|
169
|
-
process.stdout.write(`\n${c.set("CYAN", pkg.name)}: no changes\n`);
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
process.stdout.write(
|
|
174
|
-
`\n${c.set("CYAN", pkg.name)}: ${count} ${count === 1 ? "file differs" : "files differ"}\n`,
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
for (const file of pkg.added) {
|
|
178
|
-
process.stdout.write(` ${c.set("GREEN", "A")} ${file}\n`);
|
|
179
|
-
}
|
|
180
|
-
for (const file of pkg.modified) {
|
|
181
|
-
process.stdout.write(` ${c.set("ORANGE", "M")} ${file}\n`);
|
|
182
|
-
}
|
|
183
|
-
for (const file of pkg.removed) {
|
|
184
|
-
process.stdout.write(` ${c.set("RED", "D")} ${file}\n`);
|
|
185
|
-
}
|
|
150
|
+
this.printPackageDiff(pkg);
|
|
186
151
|
}
|
|
187
152
|
|
|
188
153
|
process.stdout.write("\n");
|
|
189
154
|
},
|
|
190
155
|
});
|
|
191
156
|
|
|
157
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
158
|
+
// Helpers
|
|
159
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
protected printPackageDiff(pkg: VendorPackageDiff) {
|
|
162
|
+
const c = this.color;
|
|
163
|
+
const count = pkg.added.length + pkg.modified.length + pkg.removed.length;
|
|
164
|
+
|
|
165
|
+
if (count === 0) {
|
|
166
|
+
process.stdout.write(`\n${c.set("CYAN", pkg.name)}: no changes\n`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
process.stdout.write(
|
|
171
|
+
`\n${c.set("CYAN", pkg.name)}: ${count} ${count === 1 ? "file differs" : "files differ"}\n`,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
for (const file of pkg.added) {
|
|
175
|
+
process.stdout.write(` ${c.set("GREEN", "A")} ${file}\n`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const fileDiff of pkg.modified) {
|
|
179
|
+
process.stdout.write(` ${c.set("ORANGE", "M")} ${fileDiff.file}\n`);
|
|
180
|
+
for (const change of fileDiff.changes) {
|
|
181
|
+
const prefix = change.type === "removed" ? "-" : "+";
|
|
182
|
+
const color = change.type === "removed" ? "RED" : "GREEN";
|
|
183
|
+
const lineNum = `L${change.line}`;
|
|
184
|
+
process.stdout.write(
|
|
185
|
+
` ${c.set("DIM", lineNum.padEnd(5))} ${c.set(color, `${prefix} ${change.text}`)}\n`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (const file of pkg.removed) {
|
|
191
|
+
process.stdout.write(` ${c.set("RED", "D")} ${file}\n`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
192
195
|
// ─────────────────────────────────────────────────────────────────────────
|
|
193
196
|
// Parent command
|
|
194
197
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -32,13 +32,30 @@ export interface VendorDiffOptions {
|
|
|
32
32
|
packages: string[];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* A single line change within a modified file.
|
|
37
|
+
*/
|
|
38
|
+
export interface VendorLineDiff {
|
|
39
|
+
line: number;
|
|
40
|
+
type: "added" | "removed";
|
|
41
|
+
text: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A modified file with its line-level changes.
|
|
46
|
+
*/
|
|
47
|
+
export interface VendorFileDiff {
|
|
48
|
+
file: string;
|
|
49
|
+
changes: VendorLineDiff[];
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
/**
|
|
36
53
|
* Diff result for a single vendored package.
|
|
37
54
|
*/
|
|
38
55
|
export interface VendorPackageDiff {
|
|
39
56
|
name: string;
|
|
40
57
|
added: string[];
|
|
41
|
-
modified:
|
|
58
|
+
modified: VendorFileDiff[];
|
|
42
59
|
removed: string[];
|
|
43
60
|
}
|
|
44
61
|
|
|
@@ -388,9 +405,13 @@ export class VendorService {
|
|
|
388
405
|
protected async diffDirectories(
|
|
389
406
|
localDir: string,
|
|
390
407
|
remoteDir: string,
|
|
391
|
-
): Promise<{
|
|
408
|
+
): Promise<{
|
|
409
|
+
added: string[];
|
|
410
|
+
modified: VendorFileDiff[];
|
|
411
|
+
removed: string[];
|
|
412
|
+
}> {
|
|
392
413
|
const added: string[] = [];
|
|
393
|
-
const modified:
|
|
414
|
+
const modified: VendorFileDiff[] = [];
|
|
394
415
|
const removed: string[] = [];
|
|
395
416
|
|
|
396
417
|
const [localFiles, remoteFiles] = await Promise.all([
|
|
@@ -418,7 +439,11 @@ export class VendorService {
|
|
|
418
439
|
]);
|
|
419
440
|
|
|
420
441
|
if (!localContent.equals(remoteContent)) {
|
|
421
|
-
|
|
442
|
+
const changes = this.computeLineDiff(
|
|
443
|
+
remoteContent.toString(),
|
|
444
|
+
localContent.toString(),
|
|
445
|
+
);
|
|
446
|
+
modified.push({ file, changes });
|
|
422
447
|
}
|
|
423
448
|
} catch {
|
|
424
449
|
// Skip directories and unreadable entries
|
|
@@ -434,4 +459,83 @@ export class VendorService {
|
|
|
434
459
|
|
|
435
460
|
return { added, modified, removed };
|
|
436
461
|
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Compute line-level differences between two file contents.
|
|
465
|
+
*
|
|
466
|
+
* Uses a longest-common-subsequence algorithm to produce minimal
|
|
467
|
+
* added/removed line changes with accurate line numbers.
|
|
468
|
+
*/
|
|
469
|
+
protected computeLineDiff(baseline: string, local: string): VendorLineDiff[] {
|
|
470
|
+
const baseLines = baseline.split("\n");
|
|
471
|
+
const localLines = local.split("\n");
|
|
472
|
+
const lcs = this.longestCommonSubsequence(baseLines, localLines);
|
|
473
|
+
const changes: VendorLineDiff[] = [];
|
|
474
|
+
|
|
475
|
+
let bi = 0;
|
|
476
|
+
let li = 0;
|
|
477
|
+
let ci = 0;
|
|
478
|
+
|
|
479
|
+
while (bi < baseLines.length || li < localLines.length) {
|
|
480
|
+
if (ci < lcs.length && bi < baseLines.length && li < localLines.length) {
|
|
481
|
+
if (baseLines[bi] === lcs[ci] && localLines[li] === lcs[ci]) {
|
|
482
|
+
// Line is unchanged
|
|
483
|
+
bi++;
|
|
484
|
+
li++;
|
|
485
|
+
ci++;
|
|
486
|
+
} else if (baseLines[bi] !== lcs[ci]) {
|
|
487
|
+
changes.push({ line: bi + 1, type: "removed", text: baseLines[bi] });
|
|
488
|
+
bi++;
|
|
489
|
+
} else {
|
|
490
|
+
changes.push({ line: li + 1, type: "added", text: localLines[li] });
|
|
491
|
+
li++;
|
|
492
|
+
}
|
|
493
|
+
} else if (bi < baseLines.length) {
|
|
494
|
+
changes.push({ line: bi + 1, type: "removed", text: baseLines[bi] });
|
|
495
|
+
bi++;
|
|
496
|
+
} else {
|
|
497
|
+
changes.push({ line: li + 1, type: "added", text: localLines[li] });
|
|
498
|
+
li++;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return changes;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Compute the longest common subsequence of two string arrays.
|
|
507
|
+
*/
|
|
508
|
+
protected longestCommonSubsequence(a: string[], b: string[]): string[] {
|
|
509
|
+
const m = a.length;
|
|
510
|
+
const n = b.length;
|
|
511
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () =>
|
|
512
|
+
Array(n + 1).fill(0),
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
for (let i = 1; i <= m; i++) {
|
|
516
|
+
for (let j = 1; j <= n; j++) {
|
|
517
|
+
dp[i][j] =
|
|
518
|
+
a[i - 1] === b[j - 1]
|
|
519
|
+
? dp[i - 1][j - 1] + 1
|
|
520
|
+
: Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const result: string[] = [];
|
|
525
|
+
let i = m;
|
|
526
|
+
let j = n;
|
|
527
|
+
while (i > 0 && j > 0) {
|
|
528
|
+
if (a[i - 1] === b[j - 1]) {
|
|
529
|
+
result.unshift(a[i - 1]);
|
|
530
|
+
i--;
|
|
531
|
+
j--;
|
|
532
|
+
} else if (dp[i - 1][j] > dp[i][j - 1]) {
|
|
533
|
+
i--;
|
|
534
|
+
} else {
|
|
535
|
+
j--;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return result;
|
|
540
|
+
}
|
|
437
541
|
}
|
|
@@ -26,6 +26,7 @@ class TestCliProvider extends CliProvider {
|
|
|
26
26
|
public testFindPostHooks = this.findPostHooks.bind(this);
|
|
27
27
|
public testGetEnumValues = this.getEnumValues.bind(this);
|
|
28
28
|
public testFormatFlagDescription = this.formatFlagDescription.bind(this);
|
|
29
|
+
public testParseCommandEnv = this.parseCommandEnv.bind(this);
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Extract flag definitions from a command's flags schema (for testing printHelp logic).
|
|
@@ -1291,6 +1292,50 @@ describe("CliProvider", () => {
|
|
|
1291
1292
|
});
|
|
1292
1293
|
});
|
|
1293
1294
|
|
|
1295
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1296
|
+
// parseCommandEnv
|
|
1297
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1298
|
+
|
|
1299
|
+
describe("parseCommandEnv", () => {
|
|
1300
|
+
it("should use default value for required field with default when env var is missing", () => {
|
|
1301
|
+
const cli = createTestCli();
|
|
1302
|
+
const schema = t.object({
|
|
1303
|
+
MY_VAR: t.text({ default: "fallback" }),
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
delete process.env.MY_VAR;
|
|
1307
|
+
const result = cli.testParseCommandEnv(schema, "test");
|
|
1308
|
+
expect(result.MY_VAR).toBe("fallback");
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
it("should prefer env var over default when both exist", () => {
|
|
1312
|
+
const cli = createTestCli();
|
|
1313
|
+
const schema = t.object({
|
|
1314
|
+
MY_VAR: t.text({ default: "fallback" }),
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
process.env.MY_VAR = "from-env";
|
|
1318
|
+
try {
|
|
1319
|
+
const result = cli.testParseCommandEnv(schema, "test");
|
|
1320
|
+
expect(result.MY_VAR).toBe("from-env");
|
|
1321
|
+
} finally {
|
|
1322
|
+
delete process.env.MY_VAR;
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
it("should throw for required field without default when env var is missing", () => {
|
|
1327
|
+
const cli = createTestCli();
|
|
1328
|
+
const schema = t.object({
|
|
1329
|
+
MY_VAR: t.text(),
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
delete process.env.MY_VAR;
|
|
1333
|
+
expect(() => cli.testParseCommandEnv(schema, "test")).toThrow(
|
|
1334
|
+
"Missing required environment variable",
|
|
1335
|
+
);
|
|
1336
|
+
});
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1294
1339
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1295
1340
|
// run (test helper)
|
|
1296
1341
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -594,11 +594,10 @@ export class CliProvider {
|
|
|
594
594
|
|
|
595
595
|
if (value !== undefined) {
|
|
596
596
|
result[key] = value;
|
|
597
|
+
} else if ("default" in propSchema) {
|
|
598
|
+
result[key] = propSchema.default;
|
|
597
599
|
} else if (t.schema.isOptional(propSchema)) {
|
|
598
|
-
//
|
|
599
|
-
if ("default" in propSchema) {
|
|
600
|
-
result[key] = propSchema.default;
|
|
601
|
-
}
|
|
600
|
+
// Optional with no default — leave undefined
|
|
602
601
|
} else {
|
|
603
602
|
missing.push(key);
|
|
604
603
|
}
|
|
@@ -533,6 +533,8 @@ export abstract class Repository<T extends TObject> {
|
|
|
533
533
|
} = {},
|
|
534
534
|
opts: StatementOptions & { count?: boolean } = {},
|
|
535
535
|
): Promise<Page<PgStatic<T, R>>> {
|
|
536
|
+
// Overflow-safe: pageQuerySchema constrains size to [1, 100] and page to >= 0.
|
|
537
|
+
// With max size=100, page would need to exceed 2^45 to overflow Number.MAX_SAFE_INTEGER.
|
|
536
538
|
const limit = query.limit ?? pagination.size ?? 10;
|
|
537
539
|
const page = pagination.page ?? 0;
|
|
538
540
|
const offset = query.offset ?? page * limit;
|
|
@@ -674,7 +676,9 @@ export abstract class Repository<T extends TObject> {
|
|
|
674
676
|
.returning(this.table)
|
|
675
677
|
.then(([it]) => this.clean(it, this.entity.schema));
|
|
676
678
|
|
|
677
|
-
this.dbCache
|
|
679
|
+
this.dbCache
|
|
680
|
+
.invalidateTable(this.tableName)
|
|
681
|
+
.catch((err) => this.log.warn("Cache invalidation failed", err));
|
|
678
682
|
|
|
679
683
|
await this.alepha.events.emit("repository:create:after", {
|
|
680
684
|
tableName: this.tableName,
|
|
@@ -728,7 +732,9 @@ export abstract class Repository<T extends TObject> {
|
|
|
728
732
|
allEntities.push(...entities);
|
|
729
733
|
}
|
|
730
734
|
|
|
731
|
-
this.dbCache
|
|
735
|
+
this.dbCache
|
|
736
|
+
.invalidateTable(this.tableName)
|
|
737
|
+
.catch((err) => this.log.warn("Cache invalidation failed", err));
|
|
732
738
|
|
|
733
739
|
await this.alepha.events.emit("repository:create:after", {
|
|
734
740
|
tableName: this.tableName,
|
|
@@ -825,7 +831,9 @@ export abstract class Repository<T extends TObject> {
|
|
|
825
831
|
.returning(this.table)
|
|
826
832
|
.then(([it]) => this.clean(it, this.entity.schema));
|
|
827
833
|
|
|
828
|
-
this.dbCache
|
|
834
|
+
this.dbCache
|
|
835
|
+
.invalidateTable(this.tableName)
|
|
836
|
+
.catch((err) => this.log.warn("Cache invalidation failed", err));
|
|
829
837
|
|
|
830
838
|
await this.alepha.events.emit("repository:create:after", {
|
|
831
839
|
tableName: this.tableName,
|
|
@@ -889,7 +897,9 @@ export abstract class Repository<T extends TObject> {
|
|
|
889
897
|
try {
|
|
890
898
|
const entity = this.clean(response[0], this.entity.schema);
|
|
891
899
|
|
|
892
|
-
this.dbCache
|
|
900
|
+
this.dbCache
|
|
901
|
+
.invalidateTable(this.tableName)
|
|
902
|
+
.catch((err) => this.log.warn("Cache invalidation failed", err));
|
|
893
903
|
|
|
894
904
|
await this.alepha.events.emit("repository:update:after", {
|
|
895
905
|
tableName: this.tableName,
|
|
@@ -1041,7 +1051,9 @@ export abstract class Repository<T extends TObject> {
|
|
|
1041
1051
|
.where(this.toSQL(where))
|
|
1042
1052
|
.returning();
|
|
1043
1053
|
|
|
1044
|
-
this.dbCache
|
|
1054
|
+
this.dbCache
|
|
1055
|
+
.invalidateTable(this.tableName)
|
|
1056
|
+
.catch((err) => this.log.warn("Cache invalidation failed", err));
|
|
1045
1057
|
|
|
1046
1058
|
await this.alepha.events.emit("repository:update:after", {
|
|
1047
1059
|
tableName: this.tableName,
|
|
@@ -1089,7 +1101,9 @@ export abstract class Repository<T extends TObject> {
|
|
|
1089
1101
|
.returning({ id: (this.table as any)[this.id.key] });
|
|
1090
1102
|
const ids = result.map((row) => row.id);
|
|
1091
1103
|
|
|
1092
|
-
this.dbCache
|
|
1104
|
+
this.dbCache
|
|
1105
|
+
.invalidateTable(this.tableName)
|
|
1106
|
+
.catch((err) => this.log.warn("Cache invalidation failed", err));
|
|
1093
1107
|
|
|
1094
1108
|
await this.alepha.events.emit("repository:delete:after", {
|
|
1095
1109
|
tableName: this.tableName,
|
|
@@ -366,6 +366,89 @@ describe("I18nProvider", () => {
|
|
|
366
366
|
);
|
|
367
367
|
});
|
|
368
368
|
|
|
369
|
+
describe("when only non-English dictionaries are registered", () => {
|
|
370
|
+
test("should default to the first registered language", async ({
|
|
371
|
+
expect,
|
|
372
|
+
}) => {
|
|
373
|
+
class App {
|
|
374
|
+
fr = $dictionary({
|
|
375
|
+
lazy: async () => ({ default: { hello: "Bonjour" } }),
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
380
|
+
const app = alepha.inject(App);
|
|
381
|
+
const i18n = alepha.inject(I18nProvider);
|
|
382
|
+
|
|
383
|
+
expect(i18n.lang).toBe("fr");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("should translate using the first registered language", async ({
|
|
387
|
+
expect,
|
|
388
|
+
}) => {
|
|
389
|
+
class App {
|
|
390
|
+
fr = $dictionary({
|
|
391
|
+
lazy: async () => ({
|
|
392
|
+
default: { greeting: "Bonjour!", settings: "Paramètres" },
|
|
393
|
+
}),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
398
|
+
const app = alepha.inject(App);
|
|
399
|
+
const i18n = alepha.inject(I18nProvider);
|
|
400
|
+
|
|
401
|
+
await alepha.start();
|
|
402
|
+
|
|
403
|
+
expect(i18n.tr("greeting")).toBe("Bonjour!");
|
|
404
|
+
expect(i18n.tr("settings")).toBe("Paramètres");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("should not include 'en' in languages list", async ({ expect }) => {
|
|
408
|
+
class App {
|
|
409
|
+
fr = $dictionary({
|
|
410
|
+
lazy: async () => ({ default: { hello: "Bonjour" } }),
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
de = $dictionary({
|
|
414
|
+
lazy: async () => ({ default: { hello: "Hallo" } }),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
419
|
+
const app = alepha.inject(App);
|
|
420
|
+
const i18n = alepha.inject(I18nProvider);
|
|
421
|
+
|
|
422
|
+
expect(i18n.languages).toEqual(["fr", "de"]);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("should fallback to the first registered language for missing keys", async ({
|
|
426
|
+
expect,
|
|
427
|
+
}) => {
|
|
428
|
+
class App {
|
|
429
|
+
fr = $dictionary({
|
|
430
|
+
lazy: async () => ({
|
|
431
|
+
default: { hello: "Bonjour", world: "Monde" },
|
|
432
|
+
}),
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
de = $dictionary({
|
|
436
|
+
lazy: async () => ({ default: { hello: "Hallo" } }),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const alepha = Alepha.create().with(AlephaReactI18n);
|
|
441
|
+
const app = alepha.inject(App);
|
|
442
|
+
const i18n = alepha.inject(I18nProvider);
|
|
443
|
+
|
|
444
|
+
await alepha.start();
|
|
445
|
+
await i18n.setLang("de");
|
|
446
|
+
|
|
447
|
+
expect(i18n.tr("hello")).toBe("Hallo");
|
|
448
|
+
expect(i18n.tr("world")).toBe("Monde"); // Falls back to FR
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
369
452
|
test("should handle partial interpolation args", async ({ expect }) => {
|
|
370
453
|
class App {
|
|
371
454
|
en = $dictionary({
|