@getstrata/starter 1.1.1 → 1.1.3
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/README.md +1 -1
- package/dist/cli.js +260 -91
- package/dist/templates/package.json +4 -3
- package/dist/templates/strata.config.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Pick one database engine. Docker Compose is optional (`--docker`, `--no-docker`,
|
|
|
34
34
|
|
|
35
35
|
A Bun and TypeScript app on `@getstrata/core` and `@getstrata/bootstrap` that boots, migrates, and passes `bun run check`. Cookie apps include a restyleable welcome page, `/login`, `/register`, and password reset. Token apps include `POST /api/v1/auth/login` plus register and reset. Tenancy is `none`, `column` (any engine), or `rls` (Postgres only; other engines fall back to `column`). Choices are recorded in `strata.layers.json`.
|
|
36
36
|
|
|
37
|
-
The `strata` binary installs into the app rather than globally, so use the generated `bun run` scripts.
|
|
37
|
+
The `strata` binary installs into the app rather than globally, so use the generated `bun run` scripts. Generated apps ship `src/cli/register.ts` with `queue:work` (`bun run queue:work`, requires `REDIS_URL`), `make:*`, `openapi:*`, and `schedule:run`. `queue:work` boots that app rather than the framework-repo worker.
|
|
38
38
|
|
|
39
39
|
## Docs
|
|
40
40
|
|
package/dist/cli.js
CHANGED
|
@@ -2638,6 +2638,8 @@ function renderEnvExample(projectName, layers) {
|
|
|
2638
2638
|
];
|
|
2639
2639
|
lines.push("APP_DEBUG=false");
|
|
2640
2640
|
lines.push("FEATURE_PUBLIC_READS=false");
|
|
2641
|
+
lines.push("# wrapWebPublicRead / wrapPublicRead require login when false (production-safe default).");
|
|
2642
|
+
lines.push("# Local storefronts/catalogs may set true in .env. Production boot rejects FEATURE_PUBLIC_READS=true.");
|
|
2641
2643
|
lines.push("FEATURE_SIEM_EXPORT=false");
|
|
2642
2644
|
lines.push("FEATURE_REGISTRATION=true");
|
|
2643
2645
|
lines.push("FEATURE_SAML=false");
|
|
@@ -2856,9 +2858,9 @@ function renderPackageJson(projectName, options = {}) {
|
|
|
2856
2858
|
"@getstrata/cli": "workspace:*",
|
|
2857
2859
|
"@getstrata/core": "workspace:*"
|
|
2858
2860
|
} : {
|
|
2859
|
-
"@getstrata/bootstrap": "^1.1.
|
|
2860
|
-
"@getstrata/cli": "^1.1.
|
|
2861
|
-
"@getstrata/core": "^1.1.
|
|
2861
|
+
"@getstrata/bootstrap": "^1.1.3",
|
|
2862
|
+
"@getstrata/cli": "^1.1.3",
|
|
2863
|
+
"@getstrata/core": "^1.1.3"
|
|
2862
2864
|
};
|
|
2863
2865
|
coreDeps.eta = "^4.6.0";
|
|
2864
2866
|
if (options.layers?.database === "mysql") {
|
|
@@ -2869,6 +2871,7 @@ function renderPackageJson(projectName, options = {}) {
|
|
|
2869
2871
|
start: "strata start",
|
|
2870
2872
|
"db:migrate": "strata migrate",
|
|
2871
2873
|
"db:fresh": "strata migrate:fresh",
|
|
2874
|
+
"queue:work": "strata queue:work",
|
|
2872
2875
|
check: "tsc --noEmit"
|
|
2873
2876
|
};
|
|
2874
2877
|
if (options.layers && needsFrontendBuild(options.layers.frontend)) {
|
|
@@ -3081,7 +3084,7 @@ ${next.join(`
|
|
|
3081
3084
|
`)}
|
|
3082
3085
|
\`\`\`
|
|
3083
3086
|
|
|
3084
|
-
Open http://localhost:3000. Health check: \`GET /health\`.
|
|
3087
|
+
Open http://localhost:3000. Health check: \`GET /health\`. Redis worker: \`bun run queue:work\` (requires \`REDIS_URL\`).
|
|
3085
3088
|
|
|
3086
3089
|
${renderSupportingToolsReadme(layers)}${layers.auth !== "headers" ? `
|
|
3087
3090
|
Seeded login (password \`StrataDemo!ChangeMe\`):
|
|
@@ -3141,12 +3144,12 @@ The image sets \`APP_ENV=production\` and \`AUTH_DEV_HEADERS=false\`; everything
|
|
|
3141
3144
|
- Replace every \`change-me\` placeholder in \`.env\`. The guard rejects the values this generator wrote, not just empty ones.
|
|
3142
3145
|
- Set \`APP_URL\` to the public origin (for example \`https://app.example.com\`). Signed links and redirects are built from it; localhost is rejected.
|
|
3143
3146
|
- Set \`AUTH_DEV_HEADERS=false\`.
|
|
3144
|
-
- Set \`FEATURE_PUBLIC_READS=false\`.
|
|
3147
|
+
- Set \`FEATURE_PUBLIC_READS=false\`. Generated \`.env.example\` already ships \`false\` so \`wrapWebPublicRead\` requires a login. Local storefronts may set \`true\` in \`.env\` for an anonymous catalog; \`assertProductionSecrets()\` rejects \`true\` in production. Do not ship a public-reads production boot.
|
|
3145
3148
|
- Cross-origin browser calls are off in production until you set \`CORS_ALLOWED_ORIGINS\` to explicit origins. A \`*\` entry is rejected. Non-browser clients are unaffected.
|
|
3146
3149
|
- Behind a reverse proxy or load balancer, set \`TRUST_FORWARDED_FOR=true\` so throttles and session records see the client address instead of the proxy. Only the rightmost public hop of \`X-Forwarded-For\` is trusted.
|
|
3147
3150
|
${authUsesCookie(layers.auth) ? "- Set `SESSION_SECRET` to 32+ characters.\n" : ""}${authUsesToken(layers.auth) ? "- Set `TOKEN_HASH_PEPPER`.\n" : ""}${layers.extras.scim ? "- Set `SCIM_BEARER_TOKEN`.\n" : ""}${layers.extras.metrics ? "- Set `METRICS_TOKEN`.\n" : ""}${layers.tenancy === "rls" ? `- \`DATABASE_URL\` must be a \`NOBYPASSRLS\` role, not the \`postgres\` superuser. Generated apps create \`${GENERATED_POSTGRES_APP_ROLE}\` via \`db/ensure-postgres-app-role.sql\` (and Compose init on first empty volume). \`MIGRATION_DATABASE_URL\` may stay the superuser for CREATE ROLE / GRANT / migrate. Production boot rejects username \`postgres\` or \`root\`, then inspects \`pg_roles\` for \`rolsuper\` / \`rolbypassrls\`.
|
|
3148
3151
|
` : ""}
|
|
3149
|
-
\`strata start\` does not migrate when \`APP_ENV=production\`. Run \`bun run db:migrate\` as a deploy step. \`GET /health\` is 200 when \`notes\` is readable (including zero rows) and 503 when that read fails.
|
|
3152
|
+
\`strata start\` does not migrate when \`APP_ENV=production\`. Run \`bun run db:migrate\` as a deploy step. \`GET /health\` is 200 when \`notes\` is readable (including zero rows) and 503 when that read fails. Redis jobs need a worker: \`bun run queue:work\` (requires \`REDIS_URL\`).
|
|
3150
3153
|
`;
|
|
3151
3154
|
}
|
|
3152
3155
|
|
|
@@ -3473,11 +3476,19 @@ export async function closeDatabase() {
|
|
|
3473
3476
|
}
|
|
3474
3477
|
`;
|
|
3475
3478
|
}
|
|
3476
|
-
function
|
|
3479
|
+
function starterSchemaStatements(layers) {
|
|
3477
3480
|
const d = dialectFragments(layers.database);
|
|
3478
3481
|
const statements = [];
|
|
3479
3482
|
const tenancyOn = usesTenantTable(layers.tenancy);
|
|
3480
3483
|
const mfaOn = layers.extras.mfa && authNeedsUsers(layers.auth);
|
|
3484
|
+
const payloadType = layers.database === "postgres" ? "JSONB NOT NULL" : `${d.text} NOT NULL`;
|
|
3485
|
+
statements.push(`CREATE TABLE IF NOT EXISTS failed_job (
|
|
3486
|
+
id ${d.id},
|
|
3487
|
+
job_name ${d.keyText} NOT NULL,
|
|
3488
|
+
payload ${payloadType},
|
|
3489
|
+
exception ${d.text} NOT NULL,
|
|
3490
|
+
failed_at ${d.timestamp}
|
|
3491
|
+
)`);
|
|
3481
3492
|
if (tenancyOn) {
|
|
3482
3493
|
statements.push(`CREATE TABLE IF NOT EXISTS tenant (
|
|
3483
3494
|
id ${d.id},
|
|
@@ -3564,8 +3575,87 @@ function renderMigrateTs(layers) {
|
|
|
3564
3575
|
}
|
|
3565
3576
|
statements.push(generatedRlsBootstrapSql(rlsTables, userOwnedTables).trim());
|
|
3566
3577
|
}
|
|
3567
|
-
|
|
3578
|
+
return statements;
|
|
3579
|
+
}
|
|
3580
|
+
function renderStarterSchemaMigration(layers) {
|
|
3581
|
+
const statements = starterSchemaStatements(layers);
|
|
3582
|
+
const cascade = layers.database === "sqlite" ? "" : " CASCADE";
|
|
3583
|
+
const upCalls = statements.map((sql) => ` await db.unsafe(${JSON.stringify(sql)});`).join(`
|
|
3584
|
+
`);
|
|
3585
|
+
const downCalls = dropTables(layers).map((table) => ` await db.unsafe("DROP TABLE IF EXISTS ${table}${cascade}");`).join(`
|
|
3568
3586
|
`);
|
|
3587
|
+
return `import type { Migration } from "@getstrata/core/database/migrations/types";
|
|
3588
|
+
|
|
3589
|
+
const migration: Migration = {
|
|
3590
|
+
name: "0001_starter_schema",
|
|
3591
|
+
async up(db) {
|
|
3592
|
+
${upCalls}
|
|
3593
|
+
},
|
|
3594
|
+
async down(db) {
|
|
3595
|
+
${downCalls}
|
|
3596
|
+
},
|
|
3597
|
+
};
|
|
3598
|
+
|
|
3599
|
+
export default migration;
|
|
3600
|
+
`;
|
|
3601
|
+
}
|
|
3602
|
+
function renderMigrationRuntimeTs(layers) {
|
|
3603
|
+
if (layers.database === "postgres") {
|
|
3604
|
+
return `import { join } from "node:path";
|
|
3605
|
+
import { loadMigrationsFromDirectory } from "@getstrata/core/database/migrations";
|
|
3606
|
+
import type { MigrationDatabase } from "@getstrata/core/database/migrations/types";
|
|
3607
|
+
import {
|
|
3608
|
+
grantPostgresAppRolePrivileges,
|
|
3609
|
+
openPostgresAdminConnection,
|
|
3610
|
+
postgresDatabaseNameFromUrl,
|
|
3611
|
+
} from "@getstrata/core/tenant/enableTenantRls";
|
|
3612
|
+
|
|
3613
|
+
const MIGRATIONS_DIRECTORY = join(import.meta.dir, "migrations");
|
|
3614
|
+
|
|
3615
|
+
export async function loadStarterMigrations() {
|
|
3616
|
+
return loadMigrationsFromDirectory(MIGRATIONS_DIRECTORY);
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
export async function withMigrationDatabase<T>(
|
|
3620
|
+
fn: (db: MigrationDatabase) => Promise<T>,
|
|
3621
|
+
): Promise<T> {
|
|
3622
|
+
const runtimeUrl = process.env.DATABASE_URL ?? "";
|
|
3623
|
+
const admin = await openPostgresAdminConnection({
|
|
3624
|
+
runtimeUrl,
|
|
3625
|
+
migrationUrl: process.env.MIGRATION_DATABASE_URL,
|
|
3626
|
+
});
|
|
3627
|
+
try {
|
|
3628
|
+
const result = await fn(admin);
|
|
3629
|
+
await grantPostgresAppRolePrivileges(admin, {
|
|
3630
|
+
database: postgresDatabaseNameFromUrl(runtimeUrl),
|
|
3631
|
+
});
|
|
3632
|
+
return result;
|
|
3633
|
+
} finally {
|
|
3634
|
+
await admin.close?.();
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
`;
|
|
3638
|
+
}
|
|
3639
|
+
return `import { join } from "node:path";
|
|
3640
|
+
import { loadMigrationsFromDirectory } from "@getstrata/core/database/migrations";
|
|
3641
|
+
import type { MigrationDatabase } from "@getstrata/core/database/migrations/types";
|
|
3642
|
+
import { getSql } from "../bootstrap/database.ts";
|
|
3643
|
+
|
|
3644
|
+
const MIGRATIONS_DIRECTORY = join(import.meta.dir, "migrations");
|
|
3645
|
+
|
|
3646
|
+
export async function loadStarterMigrations() {
|
|
3647
|
+
return loadMigrationsFromDirectory(MIGRATIONS_DIRECTORY);
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
export async function withMigrationDatabase<T>(
|
|
3651
|
+
fn: (db: MigrationDatabase) => Promise<T>,
|
|
3652
|
+
): Promise<T> {
|
|
3653
|
+
return fn(getSql());
|
|
3654
|
+
}
|
|
3655
|
+
`;
|
|
3656
|
+
}
|
|
3657
|
+
function renderMigrateTs(layers) {
|
|
3658
|
+
const tenancyOn = usesTenantTable(layers.tenancy);
|
|
3569
3659
|
const ph = layers.database === "postgres";
|
|
3570
3660
|
const verifyOn = layers.extras.emailVerification && authNeedsUsers(layers.auth);
|
|
3571
3661
|
const verifiedNow = nowTimestampLiteral(layers.database);
|
|
@@ -3600,8 +3690,6 @@ function renderMigrateTs(layers) {
|
|
|
3600
3690
|
const hashImport = authNeedsUsers(layers.auth) ? `import { hashPassword } from "@getstrata/core/auth/password";
|
|
3601
3691
|
` : "";
|
|
3602
3692
|
const rlsBypassImport = layers.tenancy === "rls" && layers.database === "postgres" ? `import { runWithMigrationBypass } from "@getstrata/core/tenant/databaseTenantContext";
|
|
3603
|
-
` : "";
|
|
3604
|
-
const postgresRoleImport = layers.database === "postgres" ? `import { grantPostgresAppRolePrivileges, openPostgresAdminConnection, postgresDatabaseNameFromUrl } from "@getstrata/core/tenant/enableTenantRls";
|
|
3605
3693
|
` : "";
|
|
3606
3694
|
const bindSql = seedTenant || layers.tenancy === "rls" && layers.database === "postgres" ? ` const sql = getSql();
|
|
3607
3695
|
` : "";
|
|
@@ -3612,15 +3700,13 @@ function renderMigrateTs(layers) {
|
|
|
3612
3700
|
const noteCreate = tenancyOn ? `await Note.create({ body: "Welcome to Strata!", tenant_id: 1 });` : `await Note.create({ body: "Welcome to Strata!" });`;
|
|
3613
3701
|
const userImport = authNeedsUsers(layers.auth) ? `import { User } from "../models/User.ts";
|
|
3614
3702
|
` : "";
|
|
3615
|
-
const needsSqlClient =
|
|
3703
|
+
const needsSqlClient = Boolean(seedTenant) || layers.tenancy === "rls" && layers.database === "postgres";
|
|
3616
3704
|
const databaseImport = needsSqlClient ? `import { closeDatabase, getSql } from "../bootstrap/database.ts";
|
|
3617
3705
|
` : `import { closeDatabase } from "../bootstrap/database.ts";
|
|
3618
3706
|
`;
|
|
3619
|
-
return `${hashImport}${rlsBypassImport}${
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
${list}
|
|
3623
|
-
];
|
|
3707
|
+
return `${hashImport}${rlsBypassImport}${ensureImport(layers)}${databaseImport}import { migrateDatabase } from "@getstrata/core/database/migrations";
|
|
3708
|
+
import { Note } from "../models/Note.ts";
|
|
3709
|
+
${userImport}import { loadStarterMigrations, withMigrationDatabase } from "./migrationRuntime.ts";
|
|
3624
3710
|
|
|
3625
3711
|
export async function seed() {
|
|
3626
3712
|
${ensureCall(layers)}${bindSql}${seedOpen}${seedTenant}
|
|
@@ -3630,26 +3716,10 @@ ${ensureCall(layers)}${bindSql}${seedOpen}${seedTenant}
|
|
|
3630
3716
|
}
|
|
3631
3717
|
|
|
3632
3718
|
export async function migrate() {
|
|
3633
|
-
${ensureCall(layers)}
|
|
3634
|
-
|
|
3635
|
-
runtimeUrl,
|
|
3636
|
-
migrationUrl: process.env.MIGRATION_DATABASE_URL,
|
|
3719
|
+
${ensureCall(layers)} await withMigrationDatabase(async (db) => {
|
|
3720
|
+
await migrateDatabase(db, await loadStarterMigrations());
|
|
3637
3721
|
});
|
|
3638
|
-
|
|
3639
|
-
for (const statement of migrations) {
|
|
3640
|
-
await admin.unsafe(statement);
|
|
3641
|
-
}
|
|
3642
|
-
await grantPostgresAppRolePrivileges(admin, {
|
|
3643
|
-
database: postgresDatabaseNameFromUrl(runtimeUrl),
|
|
3644
|
-
});
|
|
3645
|
-
} finally {
|
|
3646
|
-
await admin.close?.();
|
|
3647
|
-
}
|
|
3648
|
-
` : ` const sql = getSql();
|
|
3649
|
-
for (const statement of migrations) {
|
|
3650
|
-
await sql.unsafe(statement);
|
|
3651
|
-
}
|
|
3652
|
-
`} await seed();
|
|
3722
|
+
await seed();
|
|
3653
3723
|
}
|
|
3654
3724
|
|
|
3655
3725
|
/** The CLI calls this after migrate() so pooled drivers do not hold the process open. */
|
|
@@ -3862,7 +3932,7 @@ export { AuthOneTimeToken };
|
|
|
3862
3932
|
`;
|
|
3863
3933
|
}
|
|
3864
3934
|
function dropTables(layers) {
|
|
3865
|
-
const ordered = [];
|
|
3935
|
+
const ordered = ["failed_job"];
|
|
3866
3936
|
if (authUsesToken(layers.auth)) {
|
|
3867
3937
|
ordered.push("api_tokens");
|
|
3868
3938
|
}
|
|
@@ -3880,31 +3950,17 @@ function dropTables(layers) {
|
|
|
3880
3950
|
}
|
|
3881
3951
|
return ordered;
|
|
3882
3952
|
}
|
|
3883
|
-
function renderPostgresAdminDropCall() {
|
|
3884
|
-
return ` await dropPostgresTablesAsAdmin(tables, {
|
|
3885
|
-
runtimeUrl: process.env.DATABASE_URL ?? "",
|
|
3886
|
-
migrationUrl: process.env.MIGRATION_DATABASE_URL,
|
|
3887
|
-
});
|
|
3888
|
-
`;
|
|
3889
|
-
}
|
|
3890
3953
|
function renderFreshTs(layers) {
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
import { migrate } from "./migrate.ts";` : `${ensureImport(layers)}import { closeDatabase, getSql } from "../bootstrap/database.ts";
|
|
3896
|
-
import { migrate } from "./migrate.ts";`;
|
|
3897
|
-
const dropBody = layers.database === "postgres" ? `${renderPostgresAdminDropCall()} await migrate();` : ` const sql = getSql();
|
|
3898
|
-
for (const table of tables) {
|
|
3899
|
-
await sql.unsafe(\`DROP TABLE IF EXISTS \${table}${cascade}\`);
|
|
3900
|
-
}
|
|
3901
|
-
await migrate();`;
|
|
3902
|
-
return `${imports}
|
|
3903
|
-
|
|
3904
|
-
const tables = ${JSON.stringify(tables)};
|
|
3954
|
+
return `${ensureImport(layers)}import { freshDatabase } from "@getstrata/core/database/migrations";
|
|
3955
|
+
import { closeDatabase } from "../bootstrap/database.ts";
|
|
3956
|
+
import { seed } from "./migrate.ts";
|
|
3957
|
+
import { loadStarterMigrations, withMigrationDatabase } from "./migrationRuntime.ts";
|
|
3905
3958
|
|
|
3906
3959
|
export async function fresh() {
|
|
3907
|
-
${ensureCall(layers)}
|
|
3960
|
+
${ensureCall(layers)} await withMigrationDatabase(async (db) => {
|
|
3961
|
+
await freshDatabase(db, await loadStarterMigrations());
|
|
3962
|
+
});
|
|
3963
|
+
await seed();
|
|
3908
3964
|
}
|
|
3909
3965
|
|
|
3910
3966
|
/** The CLI calls this after fresh() so pooled drivers do not hold the process open. */
|
|
@@ -3933,23 +3989,17 @@ if (import.meta.main) {
|
|
|
3933
3989
|
`;
|
|
3934
3990
|
}
|
|
3935
3991
|
function renderStatusTs(layers) {
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
const tables = ${JSON.stringify(tables)};
|
|
3992
|
+
return `${ensureImport(layers)}import { getMigrationStatus } from "@getstrata/core/database/migrations";
|
|
3993
|
+
import { loadStarterMigrations, withMigrationDatabase } from "./migrationRuntime.ts";
|
|
3940
3994
|
|
|
3941
3995
|
export async function status() {
|
|
3942
|
-
${ensureCall(layers)} const
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
console.log(\`- [present] \${table} (rows: \${rows[0]?.count ?? 0})\`);
|
|
3950
|
-
} catch {
|
|
3951
|
-
console.log(\`- [missing] \${table}\`);
|
|
3952
|
-
}
|
|
3996
|
+
${ensureCall(layers)} const rows = await withMigrationDatabase(async (db) => {
|
|
3997
|
+
return getMigrationStatus(db, await loadStarterMigrations());
|
|
3998
|
+
});
|
|
3999
|
+
console.log("Migrations:");
|
|
4000
|
+
for (const row of rows) {
|
|
4001
|
+
const batch = row.batch == null ? "" : \` (batch \${row.batch})\`;
|
|
4002
|
+
console.log(\`- [\${row.status}] \${row.name}\${batch}\`);
|
|
3953
4003
|
}
|
|
3954
4004
|
}
|
|
3955
4005
|
|
|
@@ -3960,28 +4010,25 @@ if (import.meta.main) {
|
|
|
3960
4010
|
`;
|
|
3961
4011
|
}
|
|
3962
4012
|
function renderRollbackTs(layers) {
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
const imports = layers.database === "postgres" ? `import { dropPostgresTablesAsAdmin } from "@getstrata/core/tenant/enableTenantRls";
|
|
3966
|
-
${ensureImport(layers)}` : `${ensureImport(layers)}import { getSql } from "../bootstrap/database.ts";`;
|
|
3967
|
-
const dropBody = layers.database === "postgres" ? `${renderPostgresAdminDropCall()} for (const table of tables) {
|
|
3968
|
-
console.log(\`dropped \${table}\`);
|
|
3969
|
-
}` : ` const sql = getSql();
|
|
3970
|
-
for (const table of tables) {
|
|
3971
|
-
await sql.unsafe(\`DROP TABLE IF EXISTS \${table}${cascade}\`);
|
|
3972
|
-
console.log(\`dropped \${table}\`);
|
|
3973
|
-
}`;
|
|
3974
|
-
return `${imports}
|
|
3975
|
-
|
|
3976
|
-
const tables = ${JSON.stringify(tables)};
|
|
4013
|
+
return `${ensureImport(layers)}import { rollbackDatabase } from "@getstrata/core/database/migrations";
|
|
4014
|
+
import { loadStarterMigrations, withMigrationDatabase } from "./migrationRuntime.ts";
|
|
3977
4015
|
|
|
3978
4016
|
export async function rollback() {
|
|
3979
|
-
${ensureCall(layers)}
|
|
4017
|
+
${ensureCall(layers)} const rolledBack = await withMigrationDatabase(async (db) => {
|
|
4018
|
+
return rollbackDatabase(db, await loadStarterMigrations(), {
|
|
4019
|
+
onMigration: (name) => {
|
|
4020
|
+
console.log(\`rolled back \${name}\`);
|
|
4021
|
+
},
|
|
4022
|
+
});
|
|
4023
|
+
});
|
|
4024
|
+
if (rolledBack === 0) {
|
|
4025
|
+
console.log("Nothing to roll back.");
|
|
4026
|
+
}
|
|
3980
4027
|
}
|
|
3981
4028
|
|
|
3982
4029
|
if (import.meta.main) {
|
|
3983
4030
|
await rollback();
|
|
3984
|
-
console.log("Rolled back
|
|
4031
|
+
console.log("Rolled back last migration batch.");
|
|
3985
4032
|
process.exit(0);
|
|
3986
4033
|
}
|
|
3987
4034
|
`;
|
|
@@ -4067,7 +4114,8 @@ export default configProvider;
|
|
|
4067
4114
|
`;
|
|
4068
4115
|
}
|
|
4069
4116
|
function renderQueueProvider() {
|
|
4070
|
-
return `import {
|
|
4117
|
+
return `import { discoverJobs } from "@getstrata/bootstrap/discoverJobs";
|
|
4118
|
+
import { registerDefaultJobs } from "@getstrata/bootstrap/queue/defaultJobs";
|
|
4071
4119
|
import type { ServiceProvider } from "@getstrata/core/contracts/di";
|
|
4072
4120
|
import { CORE_QUEUE_TOKEN } from "@getstrata/core/contracts/serviceTokens";
|
|
4073
4121
|
import {
|
|
@@ -4084,7 +4132,10 @@ const queueProvider: ServiceProvider = {
|
|
|
4084
4132
|
container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
|
|
4085
4133
|
container.set(
|
|
4086
4134
|
CORE_QUEUE_TOKEN,
|
|
4087
|
-
createAppQueue(driver, process.env.REDIS_URL, failedJobs,
|
|
4135
|
+
createAppQueue(driver, process.env.REDIS_URL, failedJobs, () => {
|
|
4136
|
+
registerDefaultJobs();
|
|
4137
|
+
discoverJobs();
|
|
4138
|
+
}),
|
|
4088
4139
|
);
|
|
4089
4140
|
},
|
|
4090
4141
|
};
|
|
@@ -4392,6 +4443,116 @@ export function buildRoutes(dependencies: AppDependencies): AppRouteMap {
|
|
|
4392
4443
|
}
|
|
4393
4444
|
`;
|
|
4394
4445
|
}
|
|
4446
|
+
function renderCliRegisterTs() {
|
|
4447
|
+
return `import type { StrataCommandMap } from "@getstrata/cli";
|
|
4448
|
+
|
|
4449
|
+
const commands: StrataCommandMap = {
|
|
4450
|
+
"make:module": async () => (await import("@getstrata/cli/scaffold")).makeModuleCommand,
|
|
4451
|
+
"make:policy": async () => (await import("@getstrata/cli/scaffold")).makePolicyCommand,
|
|
4452
|
+
"make:job": async () => (await import("@getstrata/cli/scaffold")).makeJobCommand,
|
|
4453
|
+
"make:listener": async () => (await import("@getstrata/cli/scaffold")).makeListenerCommand,
|
|
4454
|
+
"make:request": async () => (await import("@getstrata/cli/scaffold")).makeRequestCommand,
|
|
4455
|
+
"make:factory": async () => (await import("@getstrata/cli/scaffold")).makeFactoryCommand,
|
|
4456
|
+
"make:migration": async () => (await import("@getstrata/cli/scaffold")).makeMigrationCommand,
|
|
4457
|
+
"queue:work": async () => (await import("./queueWork.ts")).queueWorkCommand,
|
|
4458
|
+
"queue:failed": async () => (await import("./queueFailed.ts")).queueFailedCommand,
|
|
4459
|
+
"queue:retry": async () => (await import("./queueFailed.ts")).queueRetryCommand,
|
|
4460
|
+
"queue:flush-failed": async () => (await import("./queueFailed.ts")).queueFlushFailedCommand,
|
|
4461
|
+
"openapi:generate": async () => (await import("./openapi.ts")).openapiGenerateCommand,
|
|
4462
|
+
"openapi:validate": async () => (await import("./openapi.ts")).openapiValidateCommand,
|
|
4463
|
+
"openapi:check": async () => (await import("./openapi.ts")).openapiCheckCommand,
|
|
4464
|
+
"schedule:run": async () => (await import("./scheduleRun.ts")).scheduleRunCommand,
|
|
4465
|
+
};
|
|
4466
|
+
|
|
4467
|
+
export { commands };
|
|
4468
|
+
`;
|
|
4469
|
+
}
|
|
4470
|
+
function renderCliQueueWorkTs() {
|
|
4471
|
+
return `async function queueWorkCommand(): Promise<void> {
|
|
4472
|
+
if (!process.env.REDIS_URL) {
|
|
4473
|
+
throw new Error("queue:work requires REDIS_URL to be set.");
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
const { runQueueWorkerCommand } = await import("@getstrata/cli/queueWorker");
|
|
4477
|
+
const { bootstrapApp } = await import("../bootstrap/createApp.ts");
|
|
4478
|
+
const { closeDatabase } = await import("../bootstrap/database.ts");
|
|
4479
|
+
|
|
4480
|
+
await runQueueWorkerCommand({
|
|
4481
|
+
boot: () => bootstrapApp({ migrate: false }),
|
|
4482
|
+
close: closeDatabase,
|
|
4483
|
+
});
|
|
4484
|
+
}
|
|
4485
|
+
|
|
4486
|
+
export { queueWorkCommand };
|
|
4487
|
+
`;
|
|
4488
|
+
}
|
|
4489
|
+
function renderCliQueueFailedTs() {
|
|
4490
|
+
return `async function bootApp(): Promise<void> {
|
|
4491
|
+
const { bootstrapApp } = await import("../bootstrap/createApp.ts");
|
|
4492
|
+
await bootstrapApp({ migrate: false });
|
|
4493
|
+
}
|
|
4494
|
+
|
|
4495
|
+
async function queueFailedCommand(): Promise<void> {
|
|
4496
|
+
const { queueFailedCommand: run } = await import("@getstrata/cli/queueFailed");
|
|
4497
|
+
await run(bootApp);
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4500
|
+
async function queueRetryCommand(id?: string): Promise<void> {
|
|
4501
|
+
const { queueRetryCommand: run } = await import("@getstrata/cli/queueFailed");
|
|
4502
|
+
await run(id, bootApp);
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4505
|
+
async function queueFlushFailedCommand(): Promise<void> {
|
|
4506
|
+
const { queueFlushFailedCommand: run } = await import("@getstrata/cli/queueFailed");
|
|
4507
|
+
await run(bootApp);
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
export { queueFailedCommand, queueFlushFailedCommand, queueRetryCommand };
|
|
4511
|
+
`;
|
|
4512
|
+
}
|
|
4513
|
+
function renderCliOpenApiTs() {
|
|
4514
|
+
return `async function bootstrap() {
|
|
4515
|
+
const { createApp } = await import("../bootstrap/createApp.ts");
|
|
4516
|
+
return createApp({ migrate: false });
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4519
|
+
async function openapiGenerateCommand(): Promise<void> {
|
|
4520
|
+
const { createOpenApiGenerateCommand } = await import("@getstrata/cli/openapi");
|
|
4521
|
+
await createOpenApiGenerateCommand(bootstrap)();
|
|
4522
|
+
}
|
|
4523
|
+
|
|
4524
|
+
async function openapiValidateCommand(): Promise<void> {
|
|
4525
|
+
const { createOpenApiValidateCommand } = await import("@getstrata/cli/openapi");
|
|
4526
|
+
await createOpenApiValidateCommand(bootstrap)();
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4529
|
+
async function openapiCheckCommand(): Promise<void> {
|
|
4530
|
+
const { createOpenApiCheckCommand } = await import("@getstrata/cli/openapi");
|
|
4531
|
+
await createOpenApiCheckCommand(bootstrap)();
|
|
4532
|
+
}
|
|
4533
|
+
|
|
4534
|
+
export { openapiCheckCommand, openapiGenerateCommand, openapiValidateCommand };
|
|
4535
|
+
`;
|
|
4536
|
+
}
|
|
4537
|
+
function renderCliScheduleRunTs() {
|
|
4538
|
+
return `async function scheduleRunCommand(): Promise<void> {
|
|
4539
|
+
const { createScheduleRunCommand } = await import("@getstrata/cli/schedule");
|
|
4540
|
+
await createScheduleRunCommand(async () => {
|
|
4541
|
+
const { bootstrapApp } = await import("../bootstrap/createApp.ts");
|
|
4542
|
+
await bootstrapApp({ migrate: false });
|
|
4543
|
+
await import("../bootstrap/schedule.ts");
|
|
4544
|
+
})();
|
|
4545
|
+
}
|
|
4546
|
+
|
|
4547
|
+
export { scheduleRunCommand };
|
|
4548
|
+
`;
|
|
4549
|
+
}
|
|
4550
|
+
function renderScheduleTs() {
|
|
4551
|
+
return `import { appSchedule } from "@getstrata/core/scheduler/schedule";
|
|
4552
|
+
|
|
4553
|
+
export { appSchedule };
|
|
4554
|
+
`;
|
|
4555
|
+
}
|
|
4395
4556
|
function renderViewTs(layers) {
|
|
4396
4557
|
const authImport = authNeedsUsers(layers.auth) ? `import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
4397
4558
|
import { resolveCsrfTokenForRequest } from "@getstrata/core/http/csrfToken";
|
|
@@ -4841,6 +5002,12 @@ function writeGeneratedFiles(options) {
|
|
|
4841
5002
|
removeIfExists(join2(src, "bootstrap/ensureDatabase.ts"));
|
|
4842
5003
|
}
|
|
4843
5004
|
writeText(join2(src, "bootstrap/createApp.ts"), renderCreateAppTs(layers));
|
|
5005
|
+
writeText(join2(src, "bootstrap/schedule.ts"), renderScheduleTs());
|
|
5006
|
+
writeText(join2(src, "cli/register.ts"), renderCliRegisterTs());
|
|
5007
|
+
writeText(join2(src, "cli/queueWork.ts"), renderCliQueueWorkTs());
|
|
5008
|
+
writeText(join2(src, "cli/queueFailed.ts"), renderCliQueueFailedTs());
|
|
5009
|
+
writeText(join2(src, "cli/openapi.ts"), renderCliOpenApiTs());
|
|
5010
|
+
writeText(join2(src, "cli/scheduleRun.ts"), renderCliScheduleRunTs());
|
|
4844
5011
|
writeText(join2(src, "bootstrap/providers/config.ts"), renderConfigProvider(layers));
|
|
4845
5012
|
writeText(join2(src, "bootstrap/providers/queue.ts"), renderQueueProvider());
|
|
4846
5013
|
writeText(join2(src, "bootstrap/providers/policy.ts"), renderPolicyProvider());
|
|
@@ -4860,6 +5027,8 @@ function writeGeneratedFiles(options) {
|
|
|
4860
5027
|
removeIfExists(join2(src, "models/ApiToken.ts"));
|
|
4861
5028
|
}
|
|
4862
5029
|
writeText(join2(src, "db/migrate.ts"), renderMigrateTs(layers));
|
|
5030
|
+
writeText(join2(src, "db/migrationRuntime.ts"), renderMigrationRuntimeTs(layers));
|
|
5031
|
+
writeText(join2(src, "db/migrations/0001_starter_schema.ts"), renderStarterSchemaMigration(layers));
|
|
4863
5032
|
writeText(join2(src, "db/fresh.ts"), renderFreshTs(layers));
|
|
4864
5033
|
writeText(join2(src, "db/seed.ts"), renderSeedTs());
|
|
4865
5034
|
writeText(join2(src, "db/status.ts"), renderStatusTs(layers));
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
"start": "strata start",
|
|
9
9
|
"db:migrate": "strata migrate",
|
|
10
10
|
"db:fresh": "strata migrate:fresh",
|
|
11
|
+
"queue:work": "strata queue:work",
|
|
11
12
|
"check": "tsc --noEmit"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
|
-
"@getstrata/bootstrap": "^1.1.
|
|
15
|
-
"@getstrata/cli": "^1.1.
|
|
16
|
-
"@getstrata/core": "^1.1.
|
|
15
|
+
"@getstrata/bootstrap": "^1.1.3",
|
|
16
|
+
"@getstrata/cli": "^1.1.3",
|
|
17
|
+
"@getstrata/core": "^1.1.3",
|
|
17
18
|
"eta": "^4.6.0"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|