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/cli.js
CHANGED
@@ -744,7 +744,7 @@ console.log('Deleted:', deleted);`,
|
|
744
744
|
responseBody: `${Type}`
|
745
745
|
});
|
746
746
|
}
|
747
|
-
const fields = table.columns.map((col) =>
|
747
|
+
const fields = table.columns.map((col) => generateFieldContract(col, table));
|
748
748
|
return {
|
749
749
|
name: Type,
|
750
750
|
tableName,
|
@@ -759,13 +759,13 @@ console.log('Deleted:', deleted);`,
|
|
759
759
|
fields
|
760
760
|
};
|
761
761
|
}
|
762
|
-
function
|
762
|
+
function generateFieldContract(column, table) {
|
763
763
|
const field = {
|
764
764
|
name: column.name,
|
765
|
-
type:
|
765
|
+
type: postgresTypeToJsonType(column.pgType),
|
766
766
|
tsType: postgresTypeToTsType(column),
|
767
767
|
required: !column.nullable && !column.hasDefault,
|
768
|
-
description:
|
768
|
+
description: generateFieldDescription(column, table)
|
769
769
|
};
|
770
770
|
const fk = table.fks.find((fk2) => fk2.from.length === 1 && fk2.from[0] === column.name);
|
771
771
|
if (fk) {
|
@@ -899,7 +899,7 @@ function generateQueryParams(table) {
|
|
899
899
|
for (const col of table.columns) {
|
900
900
|
if (filterCount >= 3)
|
901
901
|
break;
|
902
|
-
const type =
|
902
|
+
const type = postgresTypeToJsonType(col.pgType);
|
903
903
|
params[col.name] = `${type} - Filter by ${col.name}`;
|
904
904
|
if (type === "string") {
|
905
905
|
params[`${col.name}_like`] = `string - Search in ${col.name}`;
|
@@ -912,7 +912,7 @@ function generateQueryParams(table) {
|
|
912
912
|
params["..."] = "Additional filters for all fields";
|
913
913
|
return params;
|
914
914
|
}
|
915
|
-
function
|
915
|
+
function postgresTypeToJsonType(pgType) {
|
916
916
|
switch (pgType) {
|
917
917
|
case "int":
|
918
918
|
case "integer":
|
@@ -946,7 +946,7 @@ function postgresTypeToJsonType2(pgType) {
|
|
946
946
|
return "string";
|
947
947
|
}
|
948
948
|
}
|
949
|
-
function
|
949
|
+
function generateFieldDescription(column, table) {
|
950
950
|
const descriptions = [];
|
951
951
|
if (column.name === "id") {
|
952
952
|
descriptions.push("Primary key");
|
@@ -2658,7 +2658,7 @@ function emitHonoRouter(tables, hasAuth, useJsExtensions) {
|
|
2658
2658
|
return `/* Generated. Do not edit. */
|
2659
2659
|
import { Hono } from "hono";
|
2660
2660
|
import { SDK_MANIFEST } from "./sdk-bundle${ext}";
|
2661
|
-
import {
|
2661
|
+
import { getContract } from "./contract${ext}";
|
2662
2662
|
${imports}
|
2663
2663
|
${hasAuth ? `export { authMiddleware } from "./auth${ext}";` : ""}
|
2664
2664
|
|
@@ -2725,20 +2725,20 @@ ${registrations}
|
|
2725
2725
|
const format = c.req.query("format") || "json";
|
2726
2726
|
|
2727
2727
|
if (format === "markdown") {
|
2728
|
-
return c.text(
|
2728
|
+
return c.text(getContract("markdown") as string, 200, {
|
2729
2729
|
"Content-Type": "text/markdown; charset=utf-8"
|
2730
2730
|
});
|
2731
2731
|
}
|
2732
2732
|
|
2733
|
-
return c.json(
|
2733
|
+
return c.json(getContract("json"));
|
2734
2734
|
});
|
2735
2735
|
|
2736
2736
|
router.get("/api/contract.json", (c) => {
|
2737
|
-
return c.json(
|
2737
|
+
return c.json(getContract("json"));
|
2738
2738
|
});
|
2739
2739
|
|
2740
2740
|
router.get("/api/contract.md", (c) => {
|
2741
|
-
return c.text(
|
2741
|
+
return c.text(getContract("markdown") as string, 200, {
|
2742
2742
|
"Content-Type": "text/markdown; charset=utf-8"
|
2743
2743
|
});
|
2744
2744
|
});
|
@@ -3287,42 +3287,6 @@ export TEST_API_URL="http://localhost:3000"
|
|
3287
3287
|
echo "⏳ Waiting for database..."
|
3288
3288
|
sleep 3
|
3289
3289
|
|
3290
|
-
# REQUIRED: Run migrations on the test database
|
3291
|
-
echo ""
|
3292
|
-
echo "\uD83D\uDCCA Database Migration Step"
|
3293
|
-
echo "========================================="
|
3294
|
-
echo ""
|
3295
|
-
echo "⚠️ IMPORTANT: You must run migrations before tests can work!"
|
3296
|
-
echo ""
|
3297
|
-
echo "Choose one of the following options:"
|
3298
|
-
echo ""
|
3299
|
-
echo "Option 1: Add your migration command (recommended):"
|
3300
|
-
echo " Uncomment and modify one of these examples:"
|
3301
|
-
echo ""
|
3302
|
-
echo " # For Prisma:"
|
3303
|
-
echo " # npx prisma migrate deploy"
|
3304
|
-
echo ""
|
3305
|
-
echo " # For Drizzle:"
|
3306
|
-
echo " # npx drizzle-kit push --config=./drizzle.config.ts"
|
3307
|
-
echo ""
|
3308
|
-
echo " # For node-pg-migrate:"
|
3309
|
-
echo " # npm run migrate up"
|
3310
|
-
echo ""
|
3311
|
-
echo " # For Knex:"
|
3312
|
-
echo " # npx knex migrate:latest"
|
3313
|
-
echo ""
|
3314
|
-
echo " # For TypeORM:"
|
3315
|
-
echo " # npm run typeorm migration:run"
|
3316
|
-
echo ""
|
3317
|
-
echo " # For custom migration scripts:"
|
3318
|
-
echo " # node ./scripts/migrate.js"
|
3319
|
-
echo ""
|
3320
|
-
echo "Option 2: Skip migrations (only if your database is already set up):"
|
3321
|
-
echo " Uncomment the line: # SKIP_MIGRATIONS=true"
|
3322
|
-
echo ""
|
3323
|
-
echo "========================================="
|
3324
|
-
echo ""
|
3325
|
-
|
3326
3290
|
# MIGRATION_COMMAND:
|
3327
3291
|
# Add your migration command here. Examples using PROJECT_ROOT:
|
3328
3292
|
# MIGRATION_COMMAND="cd $PROJECT_ROOT && npx prisma migrate deploy"
|
@@ -3340,7 +3304,7 @@ if [ -z "\${MIGRATION_COMMAND}" ] && [ -z "\${SKIP_MIGRATIONS}" ]; then
|
|
3340
3304
|
echo " 1. Set MIGRATION_COMMAND with your migration command"
|
3341
3305
|
echo " 2. Set SKIP_MIGRATIONS=true if migrations aren't needed"
|
3342
3306
|
echo ""
|
3343
|
-
echo "
|
3307
|
+
echo " See the comments above for examples."
|
3344
3308
|
echo ""
|
3345
3309
|
exit 1
|
3346
3310
|
fi
|
@@ -3354,6 +3318,8 @@ if [ ! -z "\${MIGRATION_COMMAND}" ]; then
|
|
3354
3318
|
exit 1
|
3355
3319
|
fi
|
3356
3320
|
echo "✅ Migrations completed successfully"
|
3321
|
+
echo "⏳ Waiting for database to settle..."
|
3322
|
+
sleep 2
|
3357
3323
|
elif [ "\${SKIP_MIGRATIONS}" = "true" ]; then
|
3358
3324
|
echo "⏭️ Skipping migrations (SKIP_MIGRATIONS=true)"
|
3359
3325
|
else
|
@@ -3738,282 +3704,6 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
|
|
3738
3704
|
});` : ""}`;
|
3739
3705
|
}
|
3740
3706
|
|
3741
|
-
// src/emit-api-contract.ts
|
3742
|
-
init_utils();
|
3743
|
-
function generateApiContract(model, config) {
|
3744
|
-
const resources = [];
|
3745
|
-
const relationships = [];
|
3746
|
-
const tables = Object.values(model.tables || {});
|
3747
|
-
for (const table of tables) {
|
3748
|
-
resources.push(generateResourceContract(table, model));
|
3749
|
-
for (const fk of table.fks) {
|
3750
|
-
relationships.push({
|
3751
|
-
from: table.name,
|
3752
|
-
to: fk.toTable,
|
3753
|
-
type: "many-to-one",
|
3754
|
-
description: `Each ${table.name} belongs to one ${fk.toTable}`
|
3755
|
-
});
|
3756
|
-
}
|
3757
|
-
}
|
3758
|
-
const contract = {
|
3759
|
-
version: "1.0.0",
|
3760
|
-
generatedAt: new Date().toISOString(),
|
3761
|
-
description: "Auto-generated API contract describing all available endpoints, resources, and their relationships",
|
3762
|
-
resources,
|
3763
|
-
relationships
|
3764
|
-
};
|
3765
|
-
if (config.auth?.strategy && config.auth.strategy !== "none") {
|
3766
|
-
contract.authentication = {
|
3767
|
-
type: config.auth.strategy,
|
3768
|
-
description: getAuthDescription(config.auth.strategy)
|
3769
|
-
};
|
3770
|
-
}
|
3771
|
-
return contract;
|
3772
|
-
}
|
3773
|
-
function generateResourceContract(table, model) {
|
3774
|
-
const Type = pascal(table.name);
|
3775
|
-
const basePath = `/v1/${table.name}`;
|
3776
|
-
const endpoints = [
|
3777
|
-
{
|
3778
|
-
method: "GET",
|
3779
|
-
path: basePath,
|
3780
|
-
description: `List all ${table.name} records with optional filtering, sorting, and pagination`,
|
3781
|
-
queryParameters: {
|
3782
|
-
limit: "number - Maximum number of records to return (default: 50)",
|
3783
|
-
offset: "number - Number of records to skip for pagination",
|
3784
|
-
order_by: "string - Field to sort by",
|
3785
|
-
order_dir: "string - Sort direction (asc or desc)",
|
3786
|
-
include: "string - Comma-separated list of related resources to include",
|
3787
|
-
...generateFilterParams(table)
|
3788
|
-
},
|
3789
|
-
responseBody: `Array<${Type}>`
|
3790
|
-
},
|
3791
|
-
{
|
3792
|
-
method: "GET",
|
3793
|
-
path: `${basePath}/:id`,
|
3794
|
-
description: `Get a single ${table.name} record by ID`,
|
3795
|
-
queryParameters: {
|
3796
|
-
include: "string - Comma-separated list of related resources to include"
|
3797
|
-
},
|
3798
|
-
responseBody: `${Type}`
|
3799
|
-
},
|
3800
|
-
{
|
3801
|
-
method: "POST",
|
3802
|
-
path: basePath,
|
3803
|
-
description: `Create a new ${table.name} record`,
|
3804
|
-
requestBody: `Insert${Type}`,
|
3805
|
-
responseBody: `${Type}`
|
3806
|
-
},
|
3807
|
-
{
|
3808
|
-
method: "PATCH",
|
3809
|
-
path: `${basePath}/:id`,
|
3810
|
-
description: `Update an existing ${table.name} record`,
|
3811
|
-
requestBody: `Update${Type}`,
|
3812
|
-
responseBody: `${Type}`
|
3813
|
-
},
|
3814
|
-
{
|
3815
|
-
method: "DELETE",
|
3816
|
-
path: `${basePath}/:id`,
|
3817
|
-
description: `Delete a ${table.name} record`,
|
3818
|
-
responseBody: `${Type}`
|
3819
|
-
}
|
3820
|
-
];
|
3821
|
-
const fields = table.columns.map((col) => generateFieldContract(col, table));
|
3822
|
-
return {
|
3823
|
-
name: Type,
|
3824
|
-
tableName: table.name,
|
3825
|
-
description: `Resource for managing ${table.name} records`,
|
3826
|
-
endpoints,
|
3827
|
-
fields
|
3828
|
-
};
|
3829
|
-
}
|
3830
|
-
function generateFieldContract(column, table) {
|
3831
|
-
const field = {
|
3832
|
-
name: column.name,
|
3833
|
-
type: postgresTypeToJsonType(column.pgType),
|
3834
|
-
required: !column.nullable && !column.hasDefault,
|
3835
|
-
description: generateFieldDescription(column, table)
|
3836
|
-
};
|
3837
|
-
const fk = table.fks.find((fk2) => fk2.from.length === 1 && fk2.from[0] === column.name);
|
3838
|
-
if (fk) {
|
3839
|
-
field.foreignKey = {
|
3840
|
-
table: fk.toTable,
|
3841
|
-
field: fk.to[0] || "id"
|
3842
|
-
};
|
3843
|
-
}
|
3844
|
-
return field;
|
3845
|
-
}
|
3846
|
-
function generateFieldDescription(column, table) {
|
3847
|
-
const descriptions = [];
|
3848
|
-
descriptions.push(`${postgresTypeToJsonType(column.pgType)} field`);
|
3849
|
-
if (column.name === "id") {
|
3850
|
-
descriptions.push("Unique identifier");
|
3851
|
-
} else if (column.name === "created_at") {
|
3852
|
-
descriptions.push("Timestamp when the record was created");
|
3853
|
-
} else if (column.name === "updated_at") {
|
3854
|
-
descriptions.push("Timestamp when the record was last updated");
|
3855
|
-
} else if (column.name === "deleted_at") {
|
3856
|
-
descriptions.push("Soft delete timestamp");
|
3857
|
-
} else if (column.name.endsWith("_id")) {
|
3858
|
-
const relatedTable = column.name.slice(0, -3);
|
3859
|
-
descriptions.push(`Reference to ${relatedTable}`);
|
3860
|
-
} else if (column.name.includes("email")) {
|
3861
|
-
descriptions.push("Email address");
|
3862
|
-
} else if (column.name.includes("phone")) {
|
3863
|
-
descriptions.push("Phone number");
|
3864
|
-
} else if (column.name.includes("name")) {
|
3865
|
-
descriptions.push("Name field");
|
3866
|
-
} else if (column.name.includes("description")) {
|
3867
|
-
descriptions.push("Description text");
|
3868
|
-
} else if (column.name.includes("status")) {
|
3869
|
-
descriptions.push("Status indicator");
|
3870
|
-
} else if (column.name.includes("price") || column.name.includes("amount") || column.name.includes("total")) {
|
3871
|
-
descriptions.push("Monetary value");
|
3872
|
-
}
|
3873
|
-
if (!column.nullable && !column.hasDefault) {
|
3874
|
-
descriptions.push("(required)");
|
3875
|
-
} else if (column.nullable) {
|
3876
|
-
descriptions.push("(optional)");
|
3877
|
-
}
|
3878
|
-
return descriptions.join(" - ");
|
3879
|
-
}
|
3880
|
-
function postgresTypeToJsonType(pgType) {
|
3881
|
-
switch (pgType) {
|
3882
|
-
case "int":
|
3883
|
-
case "integer":
|
3884
|
-
case "smallint":
|
3885
|
-
case "bigint":
|
3886
|
-
case "decimal":
|
3887
|
-
case "numeric":
|
3888
|
-
case "real":
|
3889
|
-
case "double precision":
|
3890
|
-
case "float":
|
3891
|
-
return "number";
|
3892
|
-
case "boolean":
|
3893
|
-
case "bool":
|
3894
|
-
return "boolean";
|
3895
|
-
case "date":
|
3896
|
-
case "timestamp":
|
3897
|
-
case "timestamptz":
|
3898
|
-
return "date/datetime";
|
3899
|
-
case "json":
|
3900
|
-
case "jsonb":
|
3901
|
-
return "object";
|
3902
|
-
case "uuid":
|
3903
|
-
return "uuid";
|
3904
|
-
case "text[]":
|
3905
|
-
case "varchar[]":
|
3906
|
-
return "array<string>";
|
3907
|
-
case "int[]":
|
3908
|
-
case "integer[]":
|
3909
|
-
return "array<number>";
|
3910
|
-
default:
|
3911
|
-
return "string";
|
3912
|
-
}
|
3913
|
-
}
|
3914
|
-
function generateFilterParams(table) {
|
3915
|
-
const filters = {};
|
3916
|
-
for (const col of table.columns) {
|
3917
|
-
const type = postgresTypeToJsonType(col.pgType);
|
3918
|
-
filters[col.name] = `${type} - Filter by exact ${col.name} value`;
|
3919
|
-
if (type === "number" || type === "date/datetime") {
|
3920
|
-
filters[`${col.name}_gt`] = `${type} - Filter where ${col.name} is greater than`;
|
3921
|
-
filters[`${col.name}_gte`] = `${type} - Filter where ${col.name} is greater than or equal`;
|
3922
|
-
filters[`${col.name}_lt`] = `${type} - Filter where ${col.name} is less than`;
|
3923
|
-
filters[`${col.name}_lte`] = `${type} - Filter where ${col.name} is less than or equal`;
|
3924
|
-
}
|
3925
|
-
if (type === "string") {
|
3926
|
-
filters[`${col.name}_like`] = `string - Filter where ${col.name} contains text (case-insensitive)`;
|
3927
|
-
}
|
3928
|
-
}
|
3929
|
-
return filters;
|
3930
|
-
}
|
3931
|
-
function getAuthDescription(strategy) {
|
3932
|
-
switch (strategy) {
|
3933
|
-
case "jwt":
|
3934
|
-
return "JWT Bearer token authentication. Include token in Authorization header: 'Bearer <token>'";
|
3935
|
-
case "apiKey":
|
3936
|
-
return "API Key authentication. Include key in the configured header (e.g., 'x-api-key')";
|
3937
|
-
default:
|
3938
|
-
return "Custom authentication strategy";
|
3939
|
-
}
|
3940
|
-
}
|
3941
|
-
function generateApiContractMarkdown(contract) {
|
3942
|
-
const lines = [];
|
3943
|
-
lines.push("# API Contract");
|
3944
|
-
lines.push("");
|
3945
|
-
lines.push(contract.description);
|
3946
|
-
lines.push("");
|
3947
|
-
lines.push(`**Version:** ${contract.version}`);
|
3948
|
-
lines.push(`**Generated:** ${new Date(contract.generatedAt).toLocaleString()}`);
|
3949
|
-
lines.push("");
|
3950
|
-
if (contract.authentication) {
|
3951
|
-
lines.push("## Authentication");
|
3952
|
-
lines.push("");
|
3953
|
-
lines.push(`**Type:** ${contract.authentication.type}`);
|
3954
|
-
lines.push("");
|
3955
|
-
lines.push(contract.authentication.description);
|
3956
|
-
lines.push("");
|
3957
|
-
}
|
3958
|
-
lines.push("## Resources");
|
3959
|
-
lines.push("");
|
3960
|
-
for (const resource of contract.resources) {
|
3961
|
-
lines.push(`### ${resource.name}`);
|
3962
|
-
lines.push("");
|
3963
|
-
lines.push(resource.description);
|
3964
|
-
lines.push("");
|
3965
|
-
lines.push("**Endpoints:**");
|
3966
|
-
lines.push("");
|
3967
|
-
for (const endpoint of resource.endpoints) {
|
3968
|
-
lines.push(`- \`${endpoint.method} ${endpoint.path}\` - ${endpoint.description}`);
|
3969
|
-
}
|
3970
|
-
lines.push("");
|
3971
|
-
lines.push("**Fields:**");
|
3972
|
-
lines.push("");
|
3973
|
-
for (const field of resource.fields) {
|
3974
|
-
const required = field.required ? " *(required)*" : "";
|
3975
|
-
const fk = field.foreignKey ? ` → ${field.foreignKey.table}` : "";
|
3976
|
-
lines.push(`- \`${field.name}\` (${field.type})${required}${fk} - ${field.description}`);
|
3977
|
-
}
|
3978
|
-
lines.push("");
|
3979
|
-
}
|
3980
|
-
if (contract.relationships.length > 0) {
|
3981
|
-
lines.push("## Relationships");
|
3982
|
-
lines.push("");
|
3983
|
-
for (const rel of contract.relationships) {
|
3984
|
-
lines.push(`- **${rel.from}** → **${rel.to}** (${rel.type}): ${rel.description}`);
|
3985
|
-
}
|
3986
|
-
lines.push("");
|
3987
|
-
}
|
3988
|
-
return lines.join(`
|
3989
|
-
`);
|
3990
|
-
}
|
3991
|
-
function emitApiContract(model, config) {
|
3992
|
-
const contract = generateApiContract(model, config);
|
3993
|
-
const contractJson = JSON.stringify(contract, null, 2);
|
3994
|
-
return `/**
|
3995
|
-
* API Contract
|
3996
|
-
*
|
3997
|
-
* This module exports the API contract that describes all available
|
3998
|
-
* endpoints, resources, and their relationships.
|
3999
|
-
*/
|
4000
|
-
|
4001
|
-
export const apiContract = ${contractJson};
|
4002
|
-
|
4003
|
-
export const apiContractMarkdown = \`${generateApiContractMarkdown(contract).replace(/`/g, "\\`")}\`;
|
4004
|
-
|
4005
|
-
/**
|
4006
|
-
* Helper to get the contract in different formats
|
4007
|
-
*/
|
4008
|
-
export function getApiContract(format: 'json' | 'markdown' = 'json') {
|
4009
|
-
if (format === 'markdown') {
|
4010
|
-
return apiContractMarkdown;
|
4011
|
-
}
|
4012
|
-
return apiContract;
|
4013
|
-
}
|
4014
|
-
`;
|
4015
|
-
}
|
4016
|
-
|
4017
3707
|
// src/index.ts
|
4018
3708
|
init_emit_sdk_contract();
|
4019
3709
|
init_utils();
|
@@ -4163,10 +3853,6 @@ async function generate(configPath) {
|
|
4163
3853
|
path: join(serverDir, "sdk-bundle.ts"),
|
4164
3854
|
content: emitSdkBundle(clientFiles, clientDir)
|
4165
3855
|
});
|
4166
|
-
files.push({
|
4167
|
-
path: join(serverDir, "api-contract.ts"),
|
4168
|
-
content: emitApiContract(model, cfg)
|
4169
|
-
});
|
4170
3856
|
const contractCode = emitUnifiedContract(model, cfg);
|
4171
3857
|
files.push({
|
4172
3858
|
path: join(serverDir, "contract.ts"),
|
@@ -4194,7 +3880,7 @@ async function generate(configPath) {
|
|
4194
3880
|
});
|
4195
3881
|
files.push({
|
4196
3882
|
path: join(testDir, "run-tests.sh"),
|
4197
|
-
content: emitTestScript(testFramework,
|
3883
|
+
content: emitTestScript(testFramework, testDir)
|
4198
3884
|
});
|
4199
3885
|
files.push({
|
4200
3886
|
path: join(testDir, ".gitignore"),
|