postgresdk 0.6.16 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -869
- package/dist/cli.js +16 -330
- package/dist/index.js +16 -330
- package/package.json +1 -1
package/dist/index.js
CHANGED
@@ -743,7 +743,7 @@ console.log('Deleted:', deleted);`,
|
|
743
743
|
responseBody: `${Type}`
|
744
744
|
});
|
745
745
|
}
|
746
|
-
const fields = table.columns.map((col) =>
|
746
|
+
const fields = table.columns.map((col) => generateFieldContract(col, table));
|
747
747
|
return {
|
748
748
|
name: Type,
|
749
749
|
tableName,
|
@@ -758,13 +758,13 @@ console.log('Deleted:', deleted);`,
|
|
758
758
|
fields
|
759
759
|
};
|
760
760
|
}
|
761
|
-
function
|
761
|
+
function generateFieldContract(column, table) {
|
762
762
|
const field = {
|
763
763
|
name: column.name,
|
764
|
-
type:
|
764
|
+
type: postgresTypeToJsonType(column.pgType),
|
765
765
|
tsType: postgresTypeToTsType(column),
|
766
766
|
required: !column.nullable && !column.hasDefault,
|
767
|
-
description:
|
767
|
+
description: generateFieldDescription(column, table)
|
768
768
|
};
|
769
769
|
const fk = table.fks.find((fk2) => fk2.from.length === 1 && fk2.from[0] === column.name);
|
770
770
|
if (fk) {
|
@@ -898,7 +898,7 @@ function generateQueryParams(table) {
|
|
898
898
|
for (const col of table.columns) {
|
899
899
|
if (filterCount >= 3)
|
900
900
|
break;
|
901
|
-
const type =
|
901
|
+
const type = postgresTypeToJsonType(col.pgType);
|
902
902
|
params[col.name] = `${type} - Filter by ${col.name}`;
|
903
903
|
if (type === "string") {
|
904
904
|
params[`${col.name}_like`] = `string - Search in ${col.name}`;
|
@@ -911,7 +911,7 @@ function generateQueryParams(table) {
|
|
911
911
|
params["..."] = "Additional filters for all fields";
|
912
912
|
return params;
|
913
913
|
}
|
914
|
-
function
|
914
|
+
function postgresTypeToJsonType(pgType) {
|
915
915
|
switch (pgType) {
|
916
916
|
case "int":
|
917
917
|
case "integer":
|
@@ -945,7 +945,7 @@ function postgresTypeToJsonType2(pgType) {
|
|
945
945
|
return "string";
|
946
946
|
}
|
947
947
|
}
|
948
|
-
function
|
948
|
+
function generateFieldDescription(column, table) {
|
949
949
|
const descriptions = [];
|
950
950
|
if (column.name === "id") {
|
951
951
|
descriptions.push("Primary key");
|
@@ -2395,7 +2395,7 @@ function emitHonoRouter(tables, hasAuth, useJsExtensions) {
|
|
2395
2395
|
return `/* Generated. Do not edit. */
|
2396
2396
|
import { Hono } from "hono";
|
2397
2397
|
import { SDK_MANIFEST } from "./sdk-bundle${ext}";
|
2398
|
-
import {
|
2398
|
+
import { getContract } from "./contract${ext}";
|
2399
2399
|
${imports}
|
2400
2400
|
${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
|
2401
2401
|
|
@@ -2462,20 +2462,20 @@ ${registrations}
|
|
2462
2462
|
const format = c.req.query("format") || "json";
|
2463
2463
|
|
2464
2464
|
if (format === "markdown") {
|
2465
|
-
return c.text(
|
2465
|
+
return c.text(getContract("markdown") as string, 200, {
|
2466
2466
|
"Content-Type": "text/markdown; charset=utf-8"
|
2467
2467
|
});
|
2468
2468
|
}
|
2469
2469
|
|
2470
|
-
return c.json(
|
2470
|
+
return c.json(getContract("json"));
|
2471
2471
|
});
|
2472
2472
|
|
2473
2473
|
router.get("/api/contract.json", (c) => {
|
2474
|
-
return c.json(
|
2474
|
+
return c.json(getContract("json"));
|
2475
2475
|
});
|
2476
2476
|
|
2477
2477
|
router.get("/api/contract.md", (c) => {
|
2478
|
-
return c.text(
|
2478
|
+
return c.text(getContract("markdown") as string, 200, {
|
2479
2479
|
"Content-Type": "text/markdown; charset=utf-8"
|
2480
2480
|
});
|
2481
2481
|
});
|
@@ -3024,42 +3024,6 @@ export TEST_API_URL="http://localhost:3000"
|
|
3024
3024
|
echo "⏳ Waiting for database..."
|
3025
3025
|
sleep 3
|
3026
3026
|
|
3027
|
-
# REQUIRED: Run migrations on the test database
|
3028
|
-
echo ""
|
3029
|
-
echo "\uD83D\uDCCA Database Migration Step"
|
3030
|
-
echo "========================================="
|
3031
|
-
echo ""
|
3032
|
-
echo "⚠️ IMPORTANT: You must run migrations before tests can work!"
|
3033
|
-
echo ""
|
3034
|
-
echo "Choose one of the following options:"
|
3035
|
-
echo ""
|
3036
|
-
echo "Option 1: Add your migration command (recommended):"
|
3037
|
-
echo " Uncomment and modify one of these examples:"
|
3038
|
-
echo ""
|
3039
|
-
echo " # For Prisma:"
|
3040
|
-
echo " # npx prisma migrate deploy"
|
3041
|
-
echo ""
|
3042
|
-
echo " # For Drizzle:"
|
3043
|
-
echo " # npx drizzle-kit push --config=./drizzle.config.ts"
|
3044
|
-
echo ""
|
3045
|
-
echo " # For node-pg-migrate:"
|
3046
|
-
echo " # npm run migrate up"
|
3047
|
-
echo ""
|
3048
|
-
echo " # For Knex:"
|
3049
|
-
echo " # npx knex migrate:latest"
|
3050
|
-
echo ""
|
3051
|
-
echo " # For TypeORM:"
|
3052
|
-
echo " # npm run typeorm migration:run"
|
3053
|
-
echo ""
|
3054
|
-
echo " # For custom migration scripts:"
|
3055
|
-
echo " # node ./scripts/migrate.js"
|
3056
|
-
echo ""
|
3057
|
-
echo "Option 2: Skip migrations (only if your database is already set up):"
|
3058
|
-
echo " Uncomment the line: # SKIP_MIGRATIONS=true"
|
3059
|
-
echo ""
|
3060
|
-
echo "========================================="
|
3061
|
-
echo ""
|
3062
|
-
|
3063
3027
|
# MIGRATION_COMMAND:
|
3064
3028
|
# Add your migration command here. Examples using PROJECT_ROOT:
|
3065
3029
|
# MIGRATION_COMMAND="cd $PROJECT_ROOT && npx prisma migrate deploy"
|
@@ -3077,7 +3041,7 @@ if [ -z "\${MIGRATION_COMMAND}" ] && [ -z "\${SKIP_MIGRATIONS}" ]; then
|
|
3077
3041
|
echo " 1. Set MIGRATION_COMMAND with your migration command"
|
3078
3042
|
echo " 2. Set SKIP_MIGRATIONS=true if migrations aren't needed"
|
3079
3043
|
echo ""
|
3080
|
-
echo "
|
3044
|
+
echo " See the comments above for examples."
|
3081
3045
|
echo ""
|
3082
3046
|
exit 1
|
3083
3047
|
fi
|
@@ -3091,6 +3055,8 @@ if [ ! -z "\${MIGRATION_COMMAND}" ]; then
|
|
3091
3055
|
exit 1
|
3092
3056
|
fi
|
3093
3057
|
echo "✅ Migrations completed successfully"
|
3058
|
+
echo "⏳ Waiting for database to settle..."
|
3059
|
+
sleep 2
|
3094
3060
|
elif [ "\${SKIP_MIGRATIONS}" = "true" ]; then
|
3095
3061
|
echo "⏭️ Skipping migrations (SKIP_MIGRATIONS=true)"
|
3096
3062
|
else
|
@@ -3475,282 +3441,6 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
|
|
3475
3441
|
});` : ""}`;
|
3476
3442
|
}
|
3477
3443
|
|
3478
|
-
// src/emit-api-contract.ts
|
3479
|
-
init_utils();
|
3480
|
-
function generateApiContract(model, config) {
|
3481
|
-
const resources = [];
|
3482
|
-
const relationships = [];
|
3483
|
-
const tables = Object.values(model.tables || {});
|
3484
|
-
for (const table of tables) {
|
3485
|
-
resources.push(generateResourceContract(table, model));
|
3486
|
-
for (const fk of table.fks) {
|
3487
|
-
relationships.push({
|
3488
|
-
from: table.name,
|
3489
|
-
to: fk.toTable,
|
3490
|
-
type: "many-to-one",
|
3491
|
-
description: `Each ${table.name} belongs to one ${fk.toTable}`
|
3492
|
-
});
|
3493
|
-
}
|
3494
|
-
}
|
3495
|
-
const contract = {
|
3496
|
-
version: "1.0.0",
|
3497
|
-
generatedAt: new Date().toISOString(),
|
3498
|
-
description: "Auto-generated API contract describing all available endpoints, resources, and their relationships",
|
3499
|
-
resources,
|
3500
|
-
relationships
|
3501
|
-
};
|
3502
|
-
if (config.auth?.strategy && config.auth.strategy !== "none") {
|
3503
|
-
contract.authentication = {
|
3504
|
-
type: config.auth.strategy,
|
3505
|
-
description: getAuthDescription(config.auth.strategy)
|
3506
|
-
};
|
3507
|
-
}
|
3508
|
-
return contract;
|
3509
|
-
}
|
3510
|
-
function generateResourceContract(table, model) {
|
3511
|
-
const Type = pascal(table.name);
|
3512
|
-
const basePath = `/v1/${table.name}`;
|
3513
|
-
const endpoints = [
|
3514
|
-
{
|
3515
|
-
method: "GET",
|
3516
|
-
path: basePath,
|
3517
|
-
description: `List all ${table.name} records with optional filtering, sorting, and pagination`,
|
3518
|
-
queryParameters: {
|
3519
|
-
limit: "number - Maximum number of records to return (default: 50)",
|
3520
|
-
offset: "number - Number of records to skip for pagination",
|
3521
|
-
order_by: "string - Field to sort by",
|
3522
|
-
order_dir: "string - Sort direction (asc or desc)",
|
3523
|
-
include: "string - Comma-separated list of related resources to include",
|
3524
|
-
...generateFilterParams(table)
|
3525
|
-
},
|
3526
|
-
responseBody: `Array<${Type}>`
|
3527
|
-
},
|
3528
|
-
{
|
3529
|
-
method: "GET",
|
3530
|
-
path: `${basePath}/:id`,
|
3531
|
-
description: `Get a single ${table.name} record by ID`,
|
3532
|
-
queryParameters: {
|
3533
|
-
include: "string - Comma-separated list of related resources to include"
|
3534
|
-
},
|
3535
|
-
responseBody: `${Type}`
|
3536
|
-
},
|
3537
|
-
{
|
3538
|
-
method: "POST",
|
3539
|
-
path: basePath,
|
3540
|
-
description: `Create a new ${table.name} record`,
|
3541
|
-
requestBody: `Insert${Type}`,
|
3542
|
-
responseBody: `${Type}`
|
3543
|
-
},
|
3544
|
-
{
|
3545
|
-
method: "PATCH",
|
3546
|
-
path: `${basePath}/:id`,
|
3547
|
-
description: `Update an existing ${table.name} record`,
|
3548
|
-
requestBody: `Update${Type}`,
|
3549
|
-
responseBody: `${Type}`
|
3550
|
-
},
|
3551
|
-
{
|
3552
|
-
method: "DELETE",
|
3553
|
-
path: `${basePath}/:id`,
|
3554
|
-
description: `Delete a ${table.name} record`,
|
3555
|
-
responseBody: `${Type}`
|
3556
|
-
}
|
3557
|
-
];
|
3558
|
-
const fields = table.columns.map((col) => generateFieldContract(col, table));
|
3559
|
-
return {
|
3560
|
-
name: Type,
|
3561
|
-
tableName: table.name,
|
3562
|
-
description: `Resource for managing ${table.name} records`,
|
3563
|
-
endpoints,
|
3564
|
-
fields
|
3565
|
-
};
|
3566
|
-
}
|
3567
|
-
function generateFieldContract(column, table) {
|
3568
|
-
const field = {
|
3569
|
-
name: column.name,
|
3570
|
-
type: postgresTypeToJsonType(column.pgType),
|
3571
|
-
required: !column.nullable && !column.hasDefault,
|
3572
|
-
description: generateFieldDescription(column, table)
|
3573
|
-
};
|
3574
|
-
const fk = table.fks.find((fk2) => fk2.from.length === 1 && fk2.from[0] === column.name);
|
3575
|
-
if (fk) {
|
3576
|
-
field.foreignKey = {
|
3577
|
-
table: fk.toTable,
|
3578
|
-
field: fk.to[0] || "id"
|
3579
|
-
};
|
3580
|
-
}
|
3581
|
-
return field;
|
3582
|
-
}
|
3583
|
-
function generateFieldDescription(column, table) {
|
3584
|
-
const descriptions = [];
|
3585
|
-
descriptions.push(`${postgresTypeToJsonType(column.pgType)} field`);
|
3586
|
-
if (column.name === "id") {
|
3587
|
-
descriptions.push("Unique identifier");
|
3588
|
-
} else if (column.name === "created_at") {
|
3589
|
-
descriptions.push("Timestamp when the record was created");
|
3590
|
-
} else if (column.name === "updated_at") {
|
3591
|
-
descriptions.push("Timestamp when the record was last updated");
|
3592
|
-
} else if (column.name === "deleted_at") {
|
3593
|
-
descriptions.push("Soft delete timestamp");
|
3594
|
-
} else if (column.name.endsWith("_id")) {
|
3595
|
-
const relatedTable = column.name.slice(0, -3);
|
3596
|
-
descriptions.push(`Reference to ${relatedTable}`);
|
3597
|
-
} else if (column.name.includes("email")) {
|
3598
|
-
descriptions.push("Email address");
|
3599
|
-
} else if (column.name.includes("phone")) {
|
3600
|
-
descriptions.push("Phone number");
|
3601
|
-
} else if (column.name.includes("name")) {
|
3602
|
-
descriptions.push("Name field");
|
3603
|
-
} else if (column.name.includes("description")) {
|
3604
|
-
descriptions.push("Description text");
|
3605
|
-
} else if (column.name.includes("status")) {
|
3606
|
-
descriptions.push("Status indicator");
|
3607
|
-
} else if (column.name.includes("price") || column.name.includes("amount") || column.name.includes("total")) {
|
3608
|
-
descriptions.push("Monetary value");
|
3609
|
-
}
|
3610
|
-
if (!column.nullable && !column.hasDefault) {
|
3611
|
-
descriptions.push("(required)");
|
3612
|
-
} else if (column.nullable) {
|
3613
|
-
descriptions.push("(optional)");
|
3614
|
-
}
|
3615
|
-
return descriptions.join(" - ");
|
3616
|
-
}
|
3617
|
-
function postgresTypeToJsonType(pgType) {
|
3618
|
-
switch (pgType) {
|
3619
|
-
case "int":
|
3620
|
-
case "integer":
|
3621
|
-
case "smallint":
|
3622
|
-
case "bigint":
|
3623
|
-
case "decimal":
|
3624
|
-
case "numeric":
|
3625
|
-
case "real":
|
3626
|
-
case "double precision":
|
3627
|
-
case "float":
|
3628
|
-
return "number";
|
3629
|
-
case "boolean":
|
3630
|
-
case "bool":
|
3631
|
-
return "boolean";
|
3632
|
-
case "date":
|
3633
|
-
case "timestamp":
|
3634
|
-
case "timestamptz":
|
3635
|
-
return "date/datetime";
|
3636
|
-
case "json":
|
3637
|
-
case "jsonb":
|
3638
|
-
return "object";
|
3639
|
-
case "uuid":
|
3640
|
-
return "uuid";
|
3641
|
-
case "text[]":
|
3642
|
-
case "varchar[]":
|
3643
|
-
return "array<string>";
|
3644
|
-
case "int[]":
|
3645
|
-
case "integer[]":
|
3646
|
-
return "array<number>";
|
3647
|
-
default:
|
3648
|
-
return "string";
|
3649
|
-
}
|
3650
|
-
}
|
3651
|
-
function generateFilterParams(table) {
|
3652
|
-
const filters = {};
|
3653
|
-
for (const col of table.columns) {
|
3654
|
-
const type = postgresTypeToJsonType(col.pgType);
|
3655
|
-
filters[col.name] = `${type} - Filter by exact ${col.name} value`;
|
3656
|
-
if (type === "number" || type === "date/datetime") {
|
3657
|
-
filters[`${col.name}_gt`] = `${type} - Filter where ${col.name} is greater than`;
|
3658
|
-
filters[`${col.name}_gte`] = `${type} - Filter where ${col.name} is greater than or equal`;
|
3659
|
-
filters[`${col.name}_lt`] = `${type} - Filter where ${col.name} is less than`;
|
3660
|
-
filters[`${col.name}_lte`] = `${type} - Filter where ${col.name} is less than or equal`;
|
3661
|
-
}
|
3662
|
-
if (type === "string") {
|
3663
|
-
filters[`${col.name}_like`] = `string - Filter where ${col.name} contains text (case-insensitive)`;
|
3664
|
-
}
|
3665
|
-
}
|
3666
|
-
return filters;
|
3667
|
-
}
|
3668
|
-
function getAuthDescription(strategy) {
|
3669
|
-
switch (strategy) {
|
3670
|
-
case "jwt":
|
3671
|
-
return "JWT Bearer token authentication. Include token in Authorization header: 'Bearer <token>'";
|
3672
|
-
case "apiKey":
|
3673
|
-
return "API Key authentication. Include key in the configured header (e.g., 'x-api-key')";
|
3674
|
-
default:
|
3675
|
-
return "Custom authentication strategy";
|
3676
|
-
}
|
3677
|
-
}
|
3678
|
-
function generateApiContractMarkdown(contract) {
|
3679
|
-
const lines = [];
|
3680
|
-
lines.push("# API Contract");
|
3681
|
-
lines.push("");
|
3682
|
-
lines.push(contract.description);
|
3683
|
-
lines.push("");
|
3684
|
-
lines.push(`**Version:** ${contract.version}`);
|
3685
|
-
lines.push(`**Generated:** ${new Date(contract.generatedAt).toLocaleString()}`);
|
3686
|
-
lines.push("");
|
3687
|
-
if (contract.authentication) {
|
3688
|
-
lines.push("## Authentication");
|
3689
|
-
lines.push("");
|
3690
|
-
lines.push(`**Type:** ${contract.authentication.type}`);
|
3691
|
-
lines.push("");
|
3692
|
-
lines.push(contract.authentication.description);
|
3693
|
-
lines.push("");
|
3694
|
-
}
|
3695
|
-
lines.push("## Resources");
|
3696
|
-
lines.push("");
|
3697
|
-
for (const resource of contract.resources) {
|
3698
|
-
lines.push(`### ${resource.name}`);
|
3699
|
-
lines.push("");
|
3700
|
-
lines.push(resource.description);
|
3701
|
-
lines.push("");
|
3702
|
-
lines.push("**Endpoints:**");
|
3703
|
-
lines.push("");
|
3704
|
-
for (const endpoint of resource.endpoints) {
|
3705
|
-
lines.push(`- \`${endpoint.method} ${endpoint.path}\` - ${endpoint.description}`);
|
3706
|
-
}
|
3707
|
-
lines.push("");
|
3708
|
-
lines.push("**Fields:**");
|
3709
|
-
lines.push("");
|
3710
|
-
for (const field of resource.fields) {
|
3711
|
-
const required = field.required ? " *(required)*" : "";
|
3712
|
-
const fk = field.foreignKey ? ` → ${field.foreignKey.table}` : "";
|
3713
|
-
lines.push(`- \`${field.name}\` (${field.type})${required}${fk} - ${field.description}`);
|
3714
|
-
}
|
3715
|
-
lines.push("");
|
3716
|
-
}
|
3717
|
-
if (contract.relationships.length > 0) {
|
3718
|
-
lines.push("## Relationships");
|
3719
|
-
lines.push("");
|
3720
|
-
for (const rel of contract.relationships) {
|
3721
|
-
lines.push(`- **${rel.from}** → **${rel.to}** (${rel.type}): ${rel.description}`);
|
3722
|
-
}
|
3723
|
-
lines.push("");
|
3724
|
-
}
|
3725
|
-
return lines.join(`
|
3726
|
-
`);
|
3727
|
-
}
|
3728
|
-
function emitApiContract(model, config) {
|
3729
|
-
const contract = generateApiContract(model, config);
|
3730
|
-
const contractJson = JSON.stringify(contract, null, 2);
|
3731
|
-
return `/**
|
3732
|
-
* API Contract
|
3733
|
-
*
|
3734
|
-
* This module exports the API contract that describes all available
|
3735
|
-
* endpoints, resources, and their relationships.
|
3736
|
-
*/
|
3737
|
-
|
3738
|
-
export const apiContract = ${contractJson};
|
3739
|
-
|
3740
|
-
export const apiContractMarkdown = \`${generateApiContractMarkdown(contract).replace(/`/g, "\\`")}\`;
|
3741
|
-
|
3742
|
-
/**
|
3743
|
-
* Helper to get the contract in different formats
|
3744
|
-
*/
|
3745
|
-
export function getApiContract(format: 'json' | 'markdown' = 'json') {
|
3746
|
-
if (format === 'markdown') {
|
3747
|
-
return apiContractMarkdown;
|
3748
|
-
}
|
3749
|
-
return apiContract;
|
3750
|
-
}
|
3751
|
-
`;
|
3752
|
-
}
|
3753
|
-
|
3754
3444
|
// src/index.ts
|
3755
3445
|
init_emit_sdk_contract();
|
3756
3446
|
init_utils();
|
@@ -3900,10 +3590,6 @@ async function generate(configPath) {
|
|
3900
3590
|
path: join(serverDir, "sdk-bundle.ts"),
|
3901
3591
|
content: emitSdkBundle(clientFiles, clientDir)
|
3902
3592
|
});
|
3903
|
-
files.push({
|
3904
|
-
path: join(serverDir, "api-contract.ts"),
|
3905
|
-
content: emitApiContract(model, cfg)
|
3906
|
-
});
|
3907
3593
|
const contractCode = emitUnifiedContract(model, cfg);
|
3908
3594
|
files.push({
|
3909
3595
|
path: join(serverDir, "contract.ts"),
|
@@ -3931,7 +3617,7 @@ async function generate(configPath) {
|
|
3931
3617
|
});
|
3932
3618
|
files.push({
|
3933
3619
|
path: join(testDir, "run-tests.sh"),
|
3934
|
-
content: emitTestScript(testFramework,
|
3620
|
+
content: emitTestScript(testFramework, testDir)
|
3935
3621
|
});
|
3936
3622
|
files.push({
|
3937
3623
|
path: join(testDir, ".gitignore"),
|