@getstrata/starter 1.1.0 → 1.1.2
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 +294 -91
- package/dist/templates/package.json +4 -3
- package/dist/templates/strata.config.ts +1 -0
- package/package.json +1 -1
- package/dist/templates/src/bootstrap/createApp.ts +0 -95
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
|
@@ -2856,9 +2856,9 @@ function renderPackageJson(projectName, options = {}) {
|
|
|
2856
2856
|
"@getstrata/cli": "workspace:*",
|
|
2857
2857
|
"@getstrata/core": "workspace:*"
|
|
2858
2858
|
} : {
|
|
2859
|
-
"@getstrata/bootstrap": "^1.1.
|
|
2860
|
-
"@getstrata/cli": "^1.1.
|
|
2861
|
-
"@getstrata/core": "^1.1.
|
|
2859
|
+
"@getstrata/bootstrap": "^1.1.2",
|
|
2860
|
+
"@getstrata/cli": "^1.1.2",
|
|
2861
|
+
"@getstrata/core": "^1.1.2"
|
|
2862
2862
|
};
|
|
2863
2863
|
coreDeps.eta = "^4.6.0";
|
|
2864
2864
|
if (options.layers?.database === "mysql") {
|
|
@@ -2869,6 +2869,7 @@ function renderPackageJson(projectName, options = {}) {
|
|
|
2869
2869
|
start: "strata start",
|
|
2870
2870
|
"db:migrate": "strata migrate",
|
|
2871
2871
|
"db:fresh": "strata migrate:fresh",
|
|
2872
|
+
"queue:work": "strata queue:work",
|
|
2872
2873
|
check: "tsc --noEmit"
|
|
2873
2874
|
};
|
|
2874
2875
|
if (options.layers && needsFrontendBuild(options.layers.frontend)) {
|
|
@@ -3081,7 +3082,7 @@ ${next.join(`
|
|
|
3081
3082
|
`)}
|
|
3082
3083
|
\`\`\`
|
|
3083
3084
|
|
|
3084
|
-
Open http://localhost:3000. Health check: \`GET /health\`.
|
|
3085
|
+
Open http://localhost:3000. Health check: \`GET /health\`. Redis worker: \`bun run queue:work\` (requires \`REDIS_URL\`).
|
|
3085
3086
|
|
|
3086
3087
|
${renderSupportingToolsReadme(layers)}${layers.auth !== "headers" ? `
|
|
3087
3088
|
Seeded login (password \`StrataDemo!ChangeMe\`):
|
|
@@ -3146,7 +3147,7 @@ The image sets \`APP_ENV=production\` and \`AUTH_DEV_HEADERS=false\`; everything
|
|
|
3146
3147
|
- 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
3148
|
${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
3149
|
` : ""}
|
|
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.
|
|
3150
|
+
\`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
3151
|
`;
|
|
3151
3152
|
}
|
|
3152
3153
|
|
|
@@ -3473,11 +3474,19 @@ export async function closeDatabase() {
|
|
|
3473
3474
|
}
|
|
3474
3475
|
`;
|
|
3475
3476
|
}
|
|
3476
|
-
function
|
|
3477
|
+
function starterSchemaStatements(layers) {
|
|
3477
3478
|
const d = dialectFragments(layers.database);
|
|
3478
3479
|
const statements = [];
|
|
3479
3480
|
const tenancyOn = usesTenantTable(layers.tenancy);
|
|
3480
3481
|
const mfaOn = layers.extras.mfa && authNeedsUsers(layers.auth);
|
|
3482
|
+
const payloadType = layers.database === "postgres" ? "JSONB NOT NULL" : `${d.text} NOT NULL`;
|
|
3483
|
+
statements.push(`CREATE TABLE IF NOT EXISTS failed_job (
|
|
3484
|
+
id ${d.id},
|
|
3485
|
+
job_name ${d.keyText} NOT NULL,
|
|
3486
|
+
payload ${payloadType},
|
|
3487
|
+
exception ${d.text} NOT NULL,
|
|
3488
|
+
failed_at ${d.timestamp}
|
|
3489
|
+
)`);
|
|
3481
3490
|
if (tenancyOn) {
|
|
3482
3491
|
statements.push(`CREATE TABLE IF NOT EXISTS tenant (
|
|
3483
3492
|
id ${d.id},
|
|
@@ -3564,8 +3573,87 @@ function renderMigrateTs(layers) {
|
|
|
3564
3573
|
}
|
|
3565
3574
|
statements.push(generatedRlsBootstrapSql(rlsTables, userOwnedTables).trim());
|
|
3566
3575
|
}
|
|
3567
|
-
|
|
3576
|
+
return statements;
|
|
3577
|
+
}
|
|
3578
|
+
function renderStarterSchemaMigration(layers) {
|
|
3579
|
+
const statements = starterSchemaStatements(layers);
|
|
3580
|
+
const cascade = layers.database === "sqlite" ? "" : " CASCADE";
|
|
3581
|
+
const upCalls = statements.map((sql) => ` await db.unsafe(${JSON.stringify(sql)});`).join(`
|
|
3568
3582
|
`);
|
|
3583
|
+
const downCalls = dropTables(layers).map((table) => ` await db.unsafe("DROP TABLE IF EXISTS ${table}${cascade}");`).join(`
|
|
3584
|
+
`);
|
|
3585
|
+
return `import type { Migration } from "@getstrata/core/database/migrations/types";
|
|
3586
|
+
|
|
3587
|
+
const migration: Migration = {
|
|
3588
|
+
name: "0001_starter_schema",
|
|
3589
|
+
async up(db) {
|
|
3590
|
+
${upCalls}
|
|
3591
|
+
},
|
|
3592
|
+
async down(db) {
|
|
3593
|
+
${downCalls}
|
|
3594
|
+
},
|
|
3595
|
+
};
|
|
3596
|
+
|
|
3597
|
+
export default migration;
|
|
3598
|
+
`;
|
|
3599
|
+
}
|
|
3600
|
+
function renderMigrationRuntimeTs(layers) {
|
|
3601
|
+
if (layers.database === "postgres") {
|
|
3602
|
+
return `import { join } from "node:path";
|
|
3603
|
+
import { loadMigrationsFromDirectory } from "@getstrata/core/database/migrations";
|
|
3604
|
+
import type { MigrationDatabase } from "@getstrata/core/database/migrations/types";
|
|
3605
|
+
import {
|
|
3606
|
+
grantPostgresAppRolePrivileges,
|
|
3607
|
+
openPostgresAdminConnection,
|
|
3608
|
+
postgresDatabaseNameFromUrl,
|
|
3609
|
+
} from "@getstrata/core/tenant/enableTenantRls";
|
|
3610
|
+
|
|
3611
|
+
const MIGRATIONS_DIRECTORY = join(import.meta.dir, "migrations");
|
|
3612
|
+
|
|
3613
|
+
export async function loadStarterMigrations() {
|
|
3614
|
+
return loadMigrationsFromDirectory(MIGRATIONS_DIRECTORY);
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
export async function withMigrationDatabase<T>(
|
|
3618
|
+
fn: (db: MigrationDatabase) => Promise<T>,
|
|
3619
|
+
): Promise<T> {
|
|
3620
|
+
const runtimeUrl = process.env.DATABASE_URL ?? "";
|
|
3621
|
+
const admin = await openPostgresAdminConnection({
|
|
3622
|
+
runtimeUrl,
|
|
3623
|
+
migrationUrl: process.env.MIGRATION_DATABASE_URL,
|
|
3624
|
+
});
|
|
3625
|
+
try {
|
|
3626
|
+
const result = await fn(admin);
|
|
3627
|
+
await grantPostgresAppRolePrivileges(admin, {
|
|
3628
|
+
database: postgresDatabaseNameFromUrl(runtimeUrl),
|
|
3629
|
+
});
|
|
3630
|
+
return result;
|
|
3631
|
+
} finally {
|
|
3632
|
+
await admin.close?.();
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
`;
|
|
3636
|
+
}
|
|
3637
|
+
return `import { join } from "node:path";
|
|
3638
|
+
import { loadMigrationsFromDirectory } from "@getstrata/core/database/migrations";
|
|
3639
|
+
import type { MigrationDatabase } from "@getstrata/core/database/migrations/types";
|
|
3640
|
+
import { getSql } from "../bootstrap/database.ts";
|
|
3641
|
+
|
|
3642
|
+
const MIGRATIONS_DIRECTORY = join(import.meta.dir, "migrations");
|
|
3643
|
+
|
|
3644
|
+
export async function loadStarterMigrations() {
|
|
3645
|
+
return loadMigrationsFromDirectory(MIGRATIONS_DIRECTORY);
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
export async function withMigrationDatabase<T>(
|
|
3649
|
+
fn: (db: MigrationDatabase) => Promise<T>,
|
|
3650
|
+
): Promise<T> {
|
|
3651
|
+
return fn(getSql());
|
|
3652
|
+
}
|
|
3653
|
+
`;
|
|
3654
|
+
}
|
|
3655
|
+
function renderMigrateTs(layers) {
|
|
3656
|
+
const tenancyOn = usesTenantTable(layers.tenancy);
|
|
3569
3657
|
const ph = layers.database === "postgres";
|
|
3570
3658
|
const verifyOn = layers.extras.emailVerification && authNeedsUsers(layers.auth);
|
|
3571
3659
|
const verifiedNow = nowTimestampLiteral(layers.database);
|
|
@@ -3600,8 +3688,6 @@ function renderMigrateTs(layers) {
|
|
|
3600
3688
|
const hashImport = authNeedsUsers(layers.auth) ? `import { hashPassword } from "@getstrata/core/auth/password";
|
|
3601
3689
|
` : "";
|
|
3602
3690
|
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
3691
|
` : "";
|
|
3606
3692
|
const bindSql = seedTenant || layers.tenancy === "rls" && layers.database === "postgres" ? ` const sql = getSql();
|
|
3607
3693
|
` : "";
|
|
@@ -3612,15 +3698,13 @@ function renderMigrateTs(layers) {
|
|
|
3612
3698
|
const noteCreate = tenancyOn ? `await Note.create({ body: "Welcome to Strata!", tenant_id: 1 });` : `await Note.create({ body: "Welcome to Strata!" });`;
|
|
3613
3699
|
const userImport = authNeedsUsers(layers.auth) ? `import { User } from "../models/User.ts";
|
|
3614
3700
|
` : "";
|
|
3615
|
-
const needsSqlClient =
|
|
3701
|
+
const needsSqlClient = Boolean(seedTenant) || layers.tenancy === "rls" && layers.database === "postgres";
|
|
3616
3702
|
const databaseImport = needsSqlClient ? `import { closeDatabase, getSql } from "../bootstrap/database.ts";
|
|
3617
3703
|
` : `import { closeDatabase } from "../bootstrap/database.ts";
|
|
3618
3704
|
`;
|
|
3619
|
-
return `${hashImport}${rlsBypassImport}${
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
${list}
|
|
3623
|
-
];
|
|
3705
|
+
return `${hashImport}${rlsBypassImport}${ensureImport(layers)}${databaseImport}import { migrateDatabase } from "@getstrata/core/database/migrations";
|
|
3706
|
+
import { Note } from "../models/Note.ts";
|
|
3707
|
+
${userImport}import { loadStarterMigrations, withMigrationDatabase } from "./migrationRuntime.ts";
|
|
3624
3708
|
|
|
3625
3709
|
export async function seed() {
|
|
3626
3710
|
${ensureCall(layers)}${bindSql}${seedOpen}${seedTenant}
|
|
@@ -3630,26 +3714,10 @@ ${ensureCall(layers)}${bindSql}${seedOpen}${seedTenant}
|
|
|
3630
3714
|
}
|
|
3631
3715
|
|
|
3632
3716
|
export async function migrate() {
|
|
3633
|
-
${ensureCall(layers)}
|
|
3634
|
-
|
|
3635
|
-
runtimeUrl,
|
|
3636
|
-
migrationUrl: process.env.MIGRATION_DATABASE_URL,
|
|
3717
|
+
${ensureCall(layers)} await withMigrationDatabase(async (db) => {
|
|
3718
|
+
await migrateDatabase(db, await loadStarterMigrations());
|
|
3637
3719
|
});
|
|
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();
|
|
3720
|
+
await seed();
|
|
3653
3721
|
}
|
|
3654
3722
|
|
|
3655
3723
|
/** The CLI calls this after migrate() so pooled drivers do not hold the process open. */
|
|
@@ -3862,7 +3930,7 @@ export { AuthOneTimeToken };
|
|
|
3862
3930
|
`;
|
|
3863
3931
|
}
|
|
3864
3932
|
function dropTables(layers) {
|
|
3865
|
-
const ordered = [];
|
|
3933
|
+
const ordered = ["failed_job"];
|
|
3866
3934
|
if (authUsesToken(layers.auth)) {
|
|
3867
3935
|
ordered.push("api_tokens");
|
|
3868
3936
|
}
|
|
@@ -3880,31 +3948,17 @@ function dropTables(layers) {
|
|
|
3880
3948
|
}
|
|
3881
3949
|
return ordered;
|
|
3882
3950
|
}
|
|
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
3951
|
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)};
|
|
3952
|
+
return `${ensureImport(layers)}import { freshDatabase } from "@getstrata/core/database/migrations";
|
|
3953
|
+
import { closeDatabase } from "../bootstrap/database.ts";
|
|
3954
|
+
import { seed } from "./migrate.ts";
|
|
3955
|
+
import { loadStarterMigrations, withMigrationDatabase } from "./migrationRuntime.ts";
|
|
3905
3956
|
|
|
3906
3957
|
export async function fresh() {
|
|
3907
|
-
${ensureCall(layers)}
|
|
3958
|
+
${ensureCall(layers)} await withMigrationDatabase(async (db) => {
|
|
3959
|
+
await freshDatabase(db, await loadStarterMigrations());
|
|
3960
|
+
});
|
|
3961
|
+
await seed();
|
|
3908
3962
|
}
|
|
3909
3963
|
|
|
3910
3964
|
/** The CLI calls this after fresh() so pooled drivers do not hold the process open. */
|
|
@@ -3933,23 +3987,17 @@ if (import.meta.main) {
|
|
|
3933
3987
|
`;
|
|
3934
3988
|
}
|
|
3935
3989
|
function renderStatusTs(layers) {
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
const tables = ${JSON.stringify(tables)};
|
|
3990
|
+
return `${ensureImport(layers)}import { getMigrationStatus } from "@getstrata/core/database/migrations";
|
|
3991
|
+
import { loadStarterMigrations, withMigrationDatabase } from "./migrationRuntime.ts";
|
|
3940
3992
|
|
|
3941
3993
|
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
|
-
}
|
|
3994
|
+
${ensureCall(layers)} const rows = await withMigrationDatabase(async (db) => {
|
|
3995
|
+
return getMigrationStatus(db, await loadStarterMigrations());
|
|
3996
|
+
});
|
|
3997
|
+
console.log("Migrations:");
|
|
3998
|
+
for (const row of rows) {
|
|
3999
|
+
const batch = row.batch == null ? "" : \` (batch \${row.batch})\`;
|
|
4000
|
+
console.log(\`- [\${row.status}] \${row.name}\${batch}\`);
|
|
3953
4001
|
}
|
|
3954
4002
|
}
|
|
3955
4003
|
|
|
@@ -3960,28 +4008,25 @@ if (import.meta.main) {
|
|
|
3960
4008
|
`;
|
|
3961
4009
|
}
|
|
3962
4010
|
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)};
|
|
4011
|
+
return `${ensureImport(layers)}import { rollbackDatabase } from "@getstrata/core/database/migrations";
|
|
4012
|
+
import { loadStarterMigrations, withMigrationDatabase } from "./migrationRuntime.ts";
|
|
3977
4013
|
|
|
3978
4014
|
export async function rollback() {
|
|
3979
|
-
${ensureCall(layers)}
|
|
4015
|
+
${ensureCall(layers)} const rolledBack = await withMigrationDatabase(async (db) => {
|
|
4016
|
+
return rollbackDatabase(db, await loadStarterMigrations(), {
|
|
4017
|
+
onMigration: (name) => {
|
|
4018
|
+
console.log(\`rolled back \${name}\`);
|
|
4019
|
+
},
|
|
4020
|
+
});
|
|
4021
|
+
});
|
|
4022
|
+
if (rolledBack === 0) {
|
|
4023
|
+
console.log("Nothing to roll back.");
|
|
4024
|
+
}
|
|
3980
4025
|
}
|
|
3981
4026
|
|
|
3982
4027
|
if (import.meta.main) {
|
|
3983
4028
|
await rollback();
|
|
3984
|
-
console.log("Rolled back
|
|
4029
|
+
console.log("Rolled back last migration batch.");
|
|
3985
4030
|
process.exit(0);
|
|
3986
4031
|
}
|
|
3987
4032
|
`;
|
|
@@ -4067,7 +4112,9 @@ export default configProvider;
|
|
|
4067
4112
|
`;
|
|
4068
4113
|
}
|
|
4069
4114
|
function renderQueueProvider() {
|
|
4070
|
-
return `import
|
|
4115
|
+
return `import { discoverJobs } from "@getstrata/bootstrap/discoverJobs";
|
|
4116
|
+
import { registerDefaultJobs } from "@getstrata/bootstrap/queue/defaultJobs";
|
|
4117
|
+
import type { ServiceProvider } from "@getstrata/core/contracts/di";
|
|
4071
4118
|
import { CORE_QUEUE_TOKEN } from "@getstrata/core/contracts/serviceTokens";
|
|
4072
4119
|
import {
|
|
4073
4120
|
createAppQueue,
|
|
@@ -4081,7 +4128,13 @@ const queueProvider: ServiceProvider = {
|
|
|
4081
4128
|
const driver = (process.env.QUEUE_DRIVER ?? "sync") as "sync" | "async" | "redis";
|
|
4082
4129
|
const failedJobs = createFailedJobService();
|
|
4083
4130
|
container.set(FAILED_JOB_SERVICE_TOKEN, failedJobs);
|
|
4084
|
-
container.set(
|
|
4131
|
+
container.set(
|
|
4132
|
+
CORE_QUEUE_TOKEN,
|
|
4133
|
+
createAppQueue(driver, process.env.REDIS_URL, failedJobs, () => {
|
|
4134
|
+
registerDefaultJobs();
|
|
4135
|
+
discoverJobs();
|
|
4136
|
+
}),
|
|
4137
|
+
);
|
|
4085
4138
|
},
|
|
4086
4139
|
};
|
|
4087
4140
|
|
|
@@ -4185,7 +4238,9 @@ export default policyProvider;
|
|
|
4185
4238
|
`;
|
|
4186
4239
|
}
|
|
4187
4240
|
function renderProvidersIndex() {
|
|
4188
|
-
return `import
|
|
4241
|
+
return `import { discoverListeners } from "@getstrata/bootstrap/discoverListeners";
|
|
4242
|
+
import { registerInvalidateCacheOnModelWriteListeners } from "@getstrata/bootstrap/listeners/invalidateCacheOnModelWrite";
|
|
4243
|
+
import type { ServiceProvider } from "@getstrata/core/contracts/di";
|
|
4189
4244
|
import authProvider from "./auth.ts";
|
|
4190
4245
|
import cacheProvider from "./cache.ts";
|
|
4191
4246
|
import configProvider from "./config.ts";
|
|
@@ -4193,6 +4248,30 @@ import policyProvider from "./policy.ts";
|
|
|
4193
4248
|
import queueProvider from "./queue.ts";
|
|
4194
4249
|
import storageProvider from "./storage.ts";
|
|
4195
4250
|
|
|
4251
|
+
const registeredListenerGroups = new Set<string>();
|
|
4252
|
+
|
|
4253
|
+
function registerListenerGroup(name: string, register: () => void): void {
|
|
4254
|
+
if (registeredListenerGroups.has(name)) {
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
registeredListenerGroups.add(name);
|
|
4259
|
+
register();
|
|
4260
|
+
}
|
|
4261
|
+
|
|
4262
|
+
const listenersProvider: ServiceProvider = {
|
|
4263
|
+
name: "starter.listeners",
|
|
4264
|
+
boot() {
|
|
4265
|
+
registerListenerGroup("cache.invalidate-on-model-write", () => {
|
|
4266
|
+
registerInvalidateCacheOnModelWriteListeners();
|
|
4267
|
+
});
|
|
4268
|
+
|
|
4269
|
+
for (const registerListener of discoverListeners()) {
|
|
4270
|
+
registerListener();
|
|
4271
|
+
}
|
|
4272
|
+
},
|
|
4273
|
+
};
|
|
4274
|
+
|
|
4196
4275
|
const starterProviders: ServiceProvider[] = [
|
|
4197
4276
|
configProvider,
|
|
4198
4277
|
cacheProvider,
|
|
@@ -4200,6 +4279,7 @@ const starterProviders: ServiceProvider[] = [
|
|
|
4200
4279
|
queueProvider,
|
|
4201
4280
|
authProvider,
|
|
4202
4281
|
policyProvider,
|
|
4282
|
+
listenersProvider,
|
|
4203
4283
|
];
|
|
4204
4284
|
|
|
4205
4285
|
export { starterProviders };
|
|
@@ -4228,6 +4308,7 @@ import {
|
|
|
4228
4308
|
import { mergeSpaRoutes } from "@getstrata/bootstrap/createSpaRoutes";
|
|
4229
4309
|
import {
|
|
4230
4310
|
configureModulesDirectory,
|
|
4311
|
+
discoverModules,
|
|
4231
4312
|
ensureModulesLoaded,
|
|
4232
4313
|
} from "@getstrata/bootstrap/discoverModules";
|
|
4233
4314
|
import { createHealthRoutes } from "@getstrata/bootstrap/health";
|
|
@@ -4286,6 +4367,10 @@ function createAppContext(): AppContext {
|
|
|
4286
4367
|
runProviderPhase(starterProviders, "register", context);
|
|
4287
4368
|
runProviderPhase(starterProviders, "boot", context);
|
|
4288
4369
|
|
|
4370
|
+
const moduleProviders = discoverModules().flatMap((module) => module.providers ?? []);
|
|
4371
|
+
runProviderPhase(moduleProviders, "register", context);
|
|
4372
|
+
runProviderPhase(moduleProviders, "boot", context);
|
|
4373
|
+
|
|
4289
4374
|
assertAppDependenciesComplete(dependencies);
|
|
4290
4375
|
|
|
4291
4376
|
const appContext = { container, config, dependencies: dependencies as AppDependencies };
|
|
@@ -4356,6 +4441,116 @@ export function buildRoutes(dependencies: AppDependencies): AppRouteMap {
|
|
|
4356
4441
|
}
|
|
4357
4442
|
`;
|
|
4358
4443
|
}
|
|
4444
|
+
function renderCliRegisterTs() {
|
|
4445
|
+
return `import type { StrataCommandMap } from "@getstrata/cli";
|
|
4446
|
+
|
|
4447
|
+
const commands: StrataCommandMap = {
|
|
4448
|
+
"make:module": async () => (await import("@getstrata/cli/scaffold")).makeModuleCommand,
|
|
4449
|
+
"make:policy": async () => (await import("@getstrata/cli/scaffold")).makePolicyCommand,
|
|
4450
|
+
"make:job": async () => (await import("@getstrata/cli/scaffold")).makeJobCommand,
|
|
4451
|
+
"make:listener": async () => (await import("@getstrata/cli/scaffold")).makeListenerCommand,
|
|
4452
|
+
"make:request": async () => (await import("@getstrata/cli/scaffold")).makeRequestCommand,
|
|
4453
|
+
"make:factory": async () => (await import("@getstrata/cli/scaffold")).makeFactoryCommand,
|
|
4454
|
+
"make:migration": async () => (await import("@getstrata/cli/scaffold")).makeMigrationCommand,
|
|
4455
|
+
"queue:work": async () => (await import("./queueWork.ts")).queueWorkCommand,
|
|
4456
|
+
"queue:failed": async () => (await import("./queueFailed.ts")).queueFailedCommand,
|
|
4457
|
+
"queue:retry": async () => (await import("./queueFailed.ts")).queueRetryCommand,
|
|
4458
|
+
"queue:flush-failed": async () => (await import("./queueFailed.ts")).queueFlushFailedCommand,
|
|
4459
|
+
"openapi:generate": async () => (await import("./openapi.ts")).openapiGenerateCommand,
|
|
4460
|
+
"openapi:validate": async () => (await import("./openapi.ts")).openapiValidateCommand,
|
|
4461
|
+
"openapi:check": async () => (await import("./openapi.ts")).openapiCheckCommand,
|
|
4462
|
+
"schedule:run": async () => (await import("./scheduleRun.ts")).scheduleRunCommand,
|
|
4463
|
+
};
|
|
4464
|
+
|
|
4465
|
+
export { commands };
|
|
4466
|
+
`;
|
|
4467
|
+
}
|
|
4468
|
+
function renderCliQueueWorkTs() {
|
|
4469
|
+
return `async function queueWorkCommand(): Promise<void> {
|
|
4470
|
+
if (!process.env.REDIS_URL) {
|
|
4471
|
+
throw new Error("queue:work requires REDIS_URL to be set.");
|
|
4472
|
+
}
|
|
4473
|
+
|
|
4474
|
+
const { runQueueWorkerCommand } = await import("@getstrata/cli/queueWorker");
|
|
4475
|
+
const { bootstrapApp } = await import("../bootstrap/createApp.ts");
|
|
4476
|
+
const { closeDatabase } = await import("../bootstrap/database.ts");
|
|
4477
|
+
|
|
4478
|
+
await runQueueWorkerCommand({
|
|
4479
|
+
boot: () => bootstrapApp({ migrate: false }),
|
|
4480
|
+
close: closeDatabase,
|
|
4481
|
+
});
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
export { queueWorkCommand };
|
|
4485
|
+
`;
|
|
4486
|
+
}
|
|
4487
|
+
function renderCliQueueFailedTs() {
|
|
4488
|
+
return `async function bootApp(): Promise<void> {
|
|
4489
|
+
const { bootstrapApp } = await import("../bootstrap/createApp.ts");
|
|
4490
|
+
await bootstrapApp({ migrate: false });
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
async function queueFailedCommand(): Promise<void> {
|
|
4494
|
+
const { queueFailedCommand: run } = await import("@getstrata/cli/queueFailed");
|
|
4495
|
+
await run(bootApp);
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
async function queueRetryCommand(id?: string): Promise<void> {
|
|
4499
|
+
const { queueRetryCommand: run } = await import("@getstrata/cli/queueFailed");
|
|
4500
|
+
await run(id, bootApp);
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4503
|
+
async function queueFlushFailedCommand(): Promise<void> {
|
|
4504
|
+
const { queueFlushFailedCommand: run } = await import("@getstrata/cli/queueFailed");
|
|
4505
|
+
await run(bootApp);
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4508
|
+
export { queueFailedCommand, queueFlushFailedCommand, queueRetryCommand };
|
|
4509
|
+
`;
|
|
4510
|
+
}
|
|
4511
|
+
function renderCliOpenApiTs() {
|
|
4512
|
+
return `async function bootstrap() {
|
|
4513
|
+
const { createApp } = await import("../bootstrap/createApp.ts");
|
|
4514
|
+
return createApp({ migrate: false });
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
async function openapiGenerateCommand(): Promise<void> {
|
|
4518
|
+
const { createOpenApiGenerateCommand } = await import("@getstrata/cli/openapi");
|
|
4519
|
+
await createOpenApiGenerateCommand(bootstrap)();
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
async function openapiValidateCommand(): Promise<void> {
|
|
4523
|
+
const { createOpenApiValidateCommand } = await import("@getstrata/cli/openapi");
|
|
4524
|
+
await createOpenApiValidateCommand(bootstrap)();
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4527
|
+
async function openapiCheckCommand(): Promise<void> {
|
|
4528
|
+
const { createOpenApiCheckCommand } = await import("@getstrata/cli/openapi");
|
|
4529
|
+
await createOpenApiCheckCommand(bootstrap)();
|
|
4530
|
+
}
|
|
4531
|
+
|
|
4532
|
+
export { openapiCheckCommand, openapiGenerateCommand, openapiValidateCommand };
|
|
4533
|
+
`;
|
|
4534
|
+
}
|
|
4535
|
+
function renderCliScheduleRunTs() {
|
|
4536
|
+
return `async function scheduleRunCommand(): Promise<void> {
|
|
4537
|
+
const { createScheduleRunCommand } = await import("@getstrata/cli/schedule");
|
|
4538
|
+
await createScheduleRunCommand(async () => {
|
|
4539
|
+
const { bootstrapApp } = await import("../bootstrap/createApp.ts");
|
|
4540
|
+
await bootstrapApp({ migrate: false });
|
|
4541
|
+
await import("../bootstrap/schedule.ts");
|
|
4542
|
+
})();
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
export { scheduleRunCommand };
|
|
4546
|
+
`;
|
|
4547
|
+
}
|
|
4548
|
+
function renderScheduleTs() {
|
|
4549
|
+
return `import { appSchedule } from "@getstrata/core/scheduler/schedule";
|
|
4550
|
+
|
|
4551
|
+
export { appSchedule };
|
|
4552
|
+
`;
|
|
4553
|
+
}
|
|
4359
4554
|
function renderViewTs(layers) {
|
|
4360
4555
|
const authImport = authNeedsUsers(layers.auth) ? `import { currentAuthUser } from "@getstrata/core/auth/authContext";
|
|
4361
4556
|
import { resolveCsrfTokenForRequest } from "@getstrata/core/http/csrfToken";
|
|
@@ -4805,6 +5000,12 @@ function writeGeneratedFiles(options) {
|
|
|
4805
5000
|
removeIfExists(join2(src, "bootstrap/ensureDatabase.ts"));
|
|
4806
5001
|
}
|
|
4807
5002
|
writeText(join2(src, "bootstrap/createApp.ts"), renderCreateAppTs(layers));
|
|
5003
|
+
writeText(join2(src, "bootstrap/schedule.ts"), renderScheduleTs());
|
|
5004
|
+
writeText(join2(src, "cli/register.ts"), renderCliRegisterTs());
|
|
5005
|
+
writeText(join2(src, "cli/queueWork.ts"), renderCliQueueWorkTs());
|
|
5006
|
+
writeText(join2(src, "cli/queueFailed.ts"), renderCliQueueFailedTs());
|
|
5007
|
+
writeText(join2(src, "cli/openapi.ts"), renderCliOpenApiTs());
|
|
5008
|
+
writeText(join2(src, "cli/scheduleRun.ts"), renderCliScheduleRunTs());
|
|
4808
5009
|
writeText(join2(src, "bootstrap/providers/config.ts"), renderConfigProvider(layers));
|
|
4809
5010
|
writeText(join2(src, "bootstrap/providers/queue.ts"), renderQueueProvider());
|
|
4810
5011
|
writeText(join2(src, "bootstrap/providers/policy.ts"), renderPolicyProvider());
|
|
@@ -4824,6 +5025,8 @@ function writeGeneratedFiles(options) {
|
|
|
4824
5025
|
removeIfExists(join2(src, "models/ApiToken.ts"));
|
|
4825
5026
|
}
|
|
4826
5027
|
writeText(join2(src, "db/migrate.ts"), renderMigrateTs(layers));
|
|
5028
|
+
writeText(join2(src, "db/migrationRuntime.ts"), renderMigrationRuntimeTs(layers));
|
|
5029
|
+
writeText(join2(src, "db/migrations/0001_starter_schema.ts"), renderStarterSchemaMigration(layers));
|
|
4827
5030
|
writeText(join2(src, "db/fresh.ts"), renderFreshTs(layers));
|
|
4828
5031
|
writeText(join2(src, "db/seed.ts"), renderSeedTs());
|
|
4829
5032
|
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.2",
|
|
16
|
+
"@getstrata/cli": "^1.1.2",
|
|
17
|
+
"@getstrata/core": "^1.1.2",
|
|
17
18
|
"eta": "^4.6.0"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
package/package.json
CHANGED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { runProviderPhase } from "@getstrata/bootstrap/context";
|
|
2
|
-
import {
|
|
3
|
-
type AppContext,
|
|
4
|
-
type AppDependencies,
|
|
5
|
-
type AppRouteMap,
|
|
6
|
-
assertAppDependenciesComplete,
|
|
7
|
-
type ConfigStore,
|
|
8
|
-
type MutableAppDependencies,
|
|
9
|
-
type ProviderContext,
|
|
10
|
-
ServiceContainer,
|
|
11
|
-
} from "@getstrata/bootstrap/contracts";
|
|
12
|
-
import { createWebServer } from "@getstrata/bootstrap/web/server";
|
|
13
|
-
import { isProductionEnv } from "@getstrata/core/runtime/appEnv";
|
|
14
|
-
import { setActiveApplicationContext } from "@getstrata/core/runtime/applicationRegistry";
|
|
15
|
-
import { migrate } from "../db/migrate.ts";
|
|
16
|
-
import { buildRoutes } from "../routes.ts";
|
|
17
|
-
import { loadConfig } from "./config.ts";
|
|
18
|
-
import { starterProviders } from "./providers/index.ts";
|
|
19
|
-
|
|
20
|
-
export interface BootstrapOptions {
|
|
21
|
-
migrate?: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface BootstrappedApp {
|
|
25
|
-
context: AppContext;
|
|
26
|
-
routes: AppRouteMap;
|
|
27
|
-
config: ReturnType<typeof loadConfig>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
class AppConfigStore {
|
|
31
|
-
private readonly values = new Map<string, unknown>();
|
|
32
|
-
|
|
33
|
-
set<T>(key: string, value: T): T {
|
|
34
|
-
this.values.set(key, value);
|
|
35
|
-
return value;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
get<T>(key: string): T | undefined {
|
|
39
|
-
return this.values.get(key) as T | undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
require<T>(key: string): T {
|
|
43
|
-
const value = this.get<T>(key);
|
|
44
|
-
if (value === undefined) {
|
|
45
|
-
throw new Error(`Missing required config value "${key}".`);
|
|
46
|
-
}
|
|
47
|
-
return value;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
has(key: string): boolean {
|
|
51
|
-
return this.values.has(key);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function createAppContext(): AppContext {
|
|
56
|
-
const container = new ServiceContainer();
|
|
57
|
-
const config = new AppConfigStore() as unknown as ConfigStore;
|
|
58
|
-
const dependencies: MutableAppDependencies = { container };
|
|
59
|
-
const context: ProviderContext = { container, config, dependencies };
|
|
60
|
-
|
|
61
|
-
runProviderPhase(starterProviders, "register", context);
|
|
62
|
-
runProviderPhase(starterProviders, "boot", context);
|
|
63
|
-
|
|
64
|
-
assertAppDependenciesComplete(dependencies);
|
|
65
|
-
|
|
66
|
-
const appContext = { container, config, dependencies: dependencies as AppDependencies };
|
|
67
|
-
setActiveApplicationContext(appContext as never);
|
|
68
|
-
return appContext;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export async function bootstrapApp(options: BootstrapOptions = {}): Promise<BootstrappedApp> {
|
|
72
|
-
const isProduction = isProductionEnv();
|
|
73
|
-
const { migrate: runMigrate = !isProduction } = options;
|
|
74
|
-
|
|
75
|
-
const appConfig = loadConfig();
|
|
76
|
-
const context = createAppContext();
|
|
77
|
-
|
|
78
|
-
if (runMigrate) {
|
|
79
|
-
await migrate();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const routes = buildRoutes(context.dependencies);
|
|
83
|
-
|
|
84
|
-
return { context, routes, config: appConfig };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function createAppServer(routes: AppRouteMap, port = 0) {
|
|
88
|
-
return createWebServer({
|
|
89
|
-
port,
|
|
90
|
-
publicDir: "./public",
|
|
91
|
-
routes,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export { createAppContext };
|