postgresdk 0.6.11 → 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +788 -24
- package/dist/emit-sdk-contract.d.ts +83 -0
- package/dist/index.js +788 -24
- package/package.json +8 -4
package/dist/index.js
CHANGED
@@ -469,6 +469,675 @@ var require_config = __commonJS(() => {
|
|
469
469
|
})();
|
470
470
|
});
|
471
471
|
|
472
|
+
// src/utils.ts
|
473
|
+
import { mkdir, writeFile } from "fs/promises";
|
474
|
+
import { dirname } from "path";
|
475
|
+
async function writeFiles(files) {
|
476
|
+
for (const f of files) {
|
477
|
+
await mkdir(dirname(f.path), { recursive: true });
|
478
|
+
await writeFile(f.path, f.content, "utf-8");
|
479
|
+
}
|
480
|
+
}
|
481
|
+
async function ensureDirs(dirs) {
|
482
|
+
for (const d of dirs)
|
483
|
+
await mkdir(d, { recursive: true });
|
484
|
+
}
|
485
|
+
var pascal = (s) => s.split(/[_\s-]+/).map((w) => w?.[0] ? w[0].toUpperCase() + w.slice(1) : "").join("");
|
486
|
+
var init_utils = () => {};
|
487
|
+
|
488
|
+
// src/emit-sdk-contract.ts
|
489
|
+
var exports_emit_sdk_contract = {};
|
490
|
+
__export(exports_emit_sdk_contract, {
|
491
|
+
generateUnifiedContractMarkdown: () => generateUnifiedContractMarkdown,
|
492
|
+
generateUnifiedContract: () => generateUnifiedContract,
|
493
|
+
emitUnifiedContract: () => emitUnifiedContract
|
494
|
+
});
|
495
|
+
function generateUnifiedContract(model, config) {
|
496
|
+
const resources = [];
|
497
|
+
const relationships = [];
|
498
|
+
const tables = model && model.tables ? Object.values(model.tables) : [];
|
499
|
+
if (process.env.SDK_DEBUG) {
|
500
|
+
console.log(`[SDK Contract] Processing ${tables.length} tables`);
|
501
|
+
}
|
502
|
+
for (const table of tables) {
|
503
|
+
resources.push(generateResourceWithSDK(table, model));
|
504
|
+
for (const fk of table.fks) {
|
505
|
+
relationships.push({
|
506
|
+
from: table.name,
|
507
|
+
to: fk.toTable,
|
508
|
+
type: "many-to-one",
|
509
|
+
description: `Each ${table.name} belongs to one ${fk.toTable}`
|
510
|
+
});
|
511
|
+
}
|
512
|
+
}
|
513
|
+
const contract = {
|
514
|
+
version: "2.0.0",
|
515
|
+
generatedAt: new Date().toISOString(),
|
516
|
+
description: "Unified API and SDK contract - your one-stop reference for all operations",
|
517
|
+
sdk: {
|
518
|
+
initialization: generateSDKInitExamples(),
|
519
|
+
authentication: generateSDKAuthExamples(config.auth)
|
520
|
+
},
|
521
|
+
resources,
|
522
|
+
relationships
|
523
|
+
};
|
524
|
+
return contract;
|
525
|
+
}
|
526
|
+
function generateSDKInitExamples() {
|
527
|
+
return [
|
528
|
+
{
|
529
|
+
description: "Basic initialization",
|
530
|
+
code: `import { SDK } from './client';
|
531
|
+
|
532
|
+
const sdk = new SDK({
|
533
|
+
baseUrl: 'http://localhost:3000'
|
534
|
+
});`
|
535
|
+
},
|
536
|
+
{
|
537
|
+
description: "With authentication",
|
538
|
+
code: `import { SDK } from './client';
|
539
|
+
|
540
|
+
const sdk = new SDK({
|
541
|
+
baseUrl: 'https://api.example.com',
|
542
|
+
auth: {
|
543
|
+
apiKey: process.env.API_KEY
|
544
|
+
}
|
545
|
+
});`
|
546
|
+
},
|
547
|
+
{
|
548
|
+
description: "With custom fetch (for Node.js < 18)",
|
549
|
+
code: `import { SDK } from './client';
|
550
|
+
import fetch from 'node-fetch';
|
551
|
+
|
552
|
+
const sdk = new SDK({
|
553
|
+
baseUrl: 'https://api.example.com',
|
554
|
+
fetch: fetch as any
|
555
|
+
});`
|
556
|
+
}
|
557
|
+
];
|
558
|
+
}
|
559
|
+
function generateSDKAuthExamples(auth) {
|
560
|
+
const examples = [];
|
561
|
+
if (!auth || auth.strategy === "none" || !auth.strategy) {
|
562
|
+
examples.push({
|
563
|
+
strategy: "none",
|
564
|
+
description: "No authentication required",
|
565
|
+
code: `const sdk = new SDK({
|
566
|
+
baseUrl: 'http://localhost:3000'
|
567
|
+
});`
|
568
|
+
});
|
569
|
+
}
|
570
|
+
if (auth?.strategy === "api-key") {
|
571
|
+
examples.push({
|
572
|
+
strategy: "apiKey",
|
573
|
+
description: "API Key authentication",
|
574
|
+
code: `const sdk = new SDK({
|
575
|
+
baseUrl: 'https://api.example.com',
|
576
|
+
auth: {
|
577
|
+
apiKey: 'your-api-key',
|
578
|
+
apiKeyHeader: 'x-api-key' // optional, defaults to 'x-api-key'
|
579
|
+
}
|
580
|
+
});`
|
581
|
+
});
|
582
|
+
}
|
583
|
+
if (auth?.strategy === "jwt-hs256") {
|
584
|
+
examples.push({
|
585
|
+
strategy: "jwt",
|
586
|
+
description: "JWT Bearer token authentication",
|
587
|
+
code: `const sdk = new SDK({
|
588
|
+
baseUrl: 'https://api.example.com',
|
589
|
+
auth: {
|
590
|
+
jwt: 'your-jwt-token' // or async: () => getToken()
|
591
|
+
}
|
592
|
+
});`
|
593
|
+
});
|
594
|
+
examples.push({
|
595
|
+
strategy: "jwt-async",
|
596
|
+
description: "JWT with async token provider",
|
597
|
+
code: `const sdk = new SDK({
|
598
|
+
baseUrl: 'https://api.example.com',
|
599
|
+
auth: {
|
600
|
+
jwt: async () => {
|
601
|
+
const token = await refreshToken();
|
602
|
+
return token;
|
603
|
+
}
|
604
|
+
}
|
605
|
+
});`
|
606
|
+
});
|
607
|
+
}
|
608
|
+
examples.push({
|
609
|
+
strategy: "custom",
|
610
|
+
description: "Custom headers provider",
|
611
|
+
code: `const sdk = new SDK({
|
612
|
+
baseUrl: 'https://api.example.com',
|
613
|
+
auth: async () => ({
|
614
|
+
'Authorization': 'Bearer ' + await getToken(),
|
615
|
+
'X-Request-ID': generateRequestId()
|
616
|
+
})
|
617
|
+
});`
|
618
|
+
});
|
619
|
+
return examples;
|
620
|
+
}
|
621
|
+
function generateResourceWithSDK(table, model) {
|
622
|
+
const Type = pascal(table.name);
|
623
|
+
const tableName = table.name;
|
624
|
+
const basePath = `/v1/${tableName}`;
|
625
|
+
const hasSinglePK = table.pk.length === 1;
|
626
|
+
const pkField = hasSinglePK ? table.pk[0] : "id";
|
627
|
+
const sdkMethods = [];
|
628
|
+
const endpoints = [];
|
629
|
+
sdkMethods.push({
|
630
|
+
name: "list",
|
631
|
+
signature: `list(params?: ListParams): Promise<${Type}[]>`,
|
632
|
+
description: `List ${tableName} with filtering, sorting, and pagination`,
|
633
|
+
example: `// Get all ${tableName}
|
634
|
+
const items = await sdk.${tableName}.list();
|
635
|
+
|
636
|
+
// With filters and pagination
|
637
|
+
const filtered = await sdk.${tableName}.list({
|
638
|
+
limit: 20,
|
639
|
+
offset: 0,
|
640
|
+
${table.columns[0]?.name || "field"}_like: 'search',
|
641
|
+
order_by: '${table.columns[0]?.name || "created_at"}',
|
642
|
+
order_dir: 'desc'
|
643
|
+
});
|
644
|
+
|
645
|
+
// With related data
|
646
|
+
const withRelations = await sdk.${tableName}.list({
|
647
|
+
include: '${table.fks[0]?.toTable || "related_table"}'
|
648
|
+
});`,
|
649
|
+
correspondsTo: `GET ${basePath}`
|
650
|
+
});
|
651
|
+
endpoints.push({
|
652
|
+
method: "GET",
|
653
|
+
path: basePath,
|
654
|
+
description: `List all ${tableName} records`,
|
655
|
+
queryParameters: generateQueryParams(table),
|
656
|
+
responseBody: `${Type}[]`
|
657
|
+
});
|
658
|
+
if (hasSinglePK) {
|
659
|
+
sdkMethods.push({
|
660
|
+
name: "getByPk",
|
661
|
+
signature: `getByPk(${pkField}: string, params?: GetParams): Promise<${Type} | null>`,
|
662
|
+
description: `Get a single ${tableName} by primary key`,
|
663
|
+
example: `// Get by ID
|
664
|
+
const item = await sdk.${tableName}.getByPk('123e4567-e89b-12d3-a456-426614174000');
|
665
|
+
|
666
|
+
// With related data
|
667
|
+
const withRelations = await sdk.${tableName}.getByPk('123', {
|
668
|
+
include: '${table.fks[0]?.toTable || "related_table"}'
|
669
|
+
});
|
670
|
+
|
671
|
+
// Check if exists
|
672
|
+
if (item === null) {
|
673
|
+
console.log('Not found');
|
674
|
+
}`,
|
675
|
+
correspondsTo: `GET ${basePath}/:${pkField}`
|
676
|
+
});
|
677
|
+
endpoints.push({
|
678
|
+
method: "GET",
|
679
|
+
path: `${basePath}/:${pkField}`,
|
680
|
+
description: `Get ${tableName} by ID`,
|
681
|
+
queryParameters: {
|
682
|
+
include: "string - Comma-separated list of related resources"
|
683
|
+
},
|
684
|
+
responseBody: `${Type}`
|
685
|
+
});
|
686
|
+
}
|
687
|
+
sdkMethods.push({
|
688
|
+
name: "create",
|
689
|
+
signature: `create(data: Insert${Type}): Promise<${Type}>`,
|
690
|
+
description: `Create a new ${tableName}`,
|
691
|
+
example: `import type { Insert${Type} } from './client/types/${tableName}';
|
692
|
+
|
693
|
+
const newItem: Insert${Type} = {
|
694
|
+
${generateExampleFields(table, "create")}
|
695
|
+
};
|
696
|
+
|
697
|
+
const created = await sdk.${tableName}.create(newItem);
|
698
|
+
console.log('Created:', created.${pkField});`,
|
699
|
+
correspondsTo: `POST ${basePath}`
|
700
|
+
});
|
701
|
+
endpoints.push({
|
702
|
+
method: "POST",
|
703
|
+
path: basePath,
|
704
|
+
description: `Create new ${tableName}`,
|
705
|
+
requestBody: `Insert${Type}`,
|
706
|
+
responseBody: `${Type}`
|
707
|
+
});
|
708
|
+
if (hasSinglePK) {
|
709
|
+
sdkMethods.push({
|
710
|
+
name: "update",
|
711
|
+
signature: `update(${pkField}: string, data: Update${Type}): Promise<${Type}>`,
|
712
|
+
description: `Update an existing ${tableName}`,
|
713
|
+
example: `import type { Update${Type} } from './client/types/${tableName}';
|
714
|
+
|
715
|
+
const updates: Update${Type} = {
|
716
|
+
${generateExampleFields(table, "update")}
|
717
|
+
};
|
718
|
+
|
719
|
+
const updated = await sdk.${tableName}.update('123', updates);`,
|
720
|
+
correspondsTo: `PATCH ${basePath}/:${pkField}`
|
721
|
+
});
|
722
|
+
endpoints.push({
|
723
|
+
method: "PATCH",
|
724
|
+
path: `${basePath}/:${pkField}`,
|
725
|
+
description: `Update ${tableName}`,
|
726
|
+
requestBody: `Update${Type}`,
|
727
|
+
responseBody: `${Type}`
|
728
|
+
});
|
729
|
+
}
|
730
|
+
if (hasSinglePK) {
|
731
|
+
sdkMethods.push({
|
732
|
+
name: "delete",
|
733
|
+
signature: `delete(${pkField}: string): Promise<${Type}>`,
|
734
|
+
description: `Delete a ${tableName}`,
|
735
|
+
example: `const deleted = await sdk.${tableName}.delete('123');
|
736
|
+
console.log('Deleted:', deleted);`,
|
737
|
+
correspondsTo: `DELETE ${basePath}/:${pkField}`
|
738
|
+
});
|
739
|
+
endpoints.push({
|
740
|
+
method: "DELETE",
|
741
|
+
path: `${basePath}/:${pkField}`,
|
742
|
+
description: `Delete ${tableName}`,
|
743
|
+
responseBody: `${Type}`
|
744
|
+
});
|
745
|
+
}
|
746
|
+
const fields = table.columns.map((col) => generateFieldContract2(col, table));
|
747
|
+
return {
|
748
|
+
name: Type,
|
749
|
+
tableName,
|
750
|
+
description: `Resource for ${tableName} operations`,
|
751
|
+
sdk: {
|
752
|
+
client: `sdk.${tableName}`,
|
753
|
+
methods: sdkMethods
|
754
|
+
},
|
755
|
+
api: {
|
756
|
+
endpoints
|
757
|
+
},
|
758
|
+
fields
|
759
|
+
};
|
760
|
+
}
|
761
|
+
function generateFieldContract2(column, table) {
|
762
|
+
const field = {
|
763
|
+
name: column.name,
|
764
|
+
type: postgresTypeToJsonType2(column.pgType),
|
765
|
+
tsType: postgresTypeToTsType(column),
|
766
|
+
required: !column.nullable && !column.hasDefault,
|
767
|
+
description: generateFieldDescription2(column, table)
|
768
|
+
};
|
769
|
+
const fk = table.fks.find((fk2) => fk2.from.length === 1 && fk2.from[0] === column.name);
|
770
|
+
if (fk) {
|
771
|
+
field.foreignKey = {
|
772
|
+
table: fk.toTable,
|
773
|
+
field: fk.to[0] || "id"
|
774
|
+
};
|
775
|
+
}
|
776
|
+
return field;
|
777
|
+
}
|
778
|
+
function postgresTypeToTsType(column) {
|
779
|
+
const baseType = (() => {
|
780
|
+
switch (column.pgType) {
|
781
|
+
case "int":
|
782
|
+
case "integer":
|
783
|
+
case "smallint":
|
784
|
+
case "bigint":
|
785
|
+
case "decimal":
|
786
|
+
case "numeric":
|
787
|
+
case "real":
|
788
|
+
case "double precision":
|
789
|
+
case "float":
|
790
|
+
return "number";
|
791
|
+
case "boolean":
|
792
|
+
case "bool":
|
793
|
+
return "boolean";
|
794
|
+
case "date":
|
795
|
+
case "timestamp":
|
796
|
+
case "timestamptz":
|
797
|
+
return "string";
|
798
|
+
case "json":
|
799
|
+
case "jsonb":
|
800
|
+
return "Record<string, any>";
|
801
|
+
case "uuid":
|
802
|
+
return "string";
|
803
|
+
case "text[]":
|
804
|
+
case "varchar[]":
|
805
|
+
return "string[]";
|
806
|
+
case "int[]":
|
807
|
+
case "integer[]":
|
808
|
+
return "number[]";
|
809
|
+
default:
|
810
|
+
return "string";
|
811
|
+
}
|
812
|
+
})();
|
813
|
+
if (column.nullable) {
|
814
|
+
return `${baseType} | null`;
|
815
|
+
}
|
816
|
+
return baseType;
|
817
|
+
}
|
818
|
+
function generateExampleFields(table, operation) {
|
819
|
+
const fields = [];
|
820
|
+
let count = 0;
|
821
|
+
for (const col of table.columns) {
|
822
|
+
if (col.hasDefault && ["id", "created_at", "updated_at"].includes(col.name)) {
|
823
|
+
continue;
|
824
|
+
}
|
825
|
+
if (operation === "update" && count >= 2) {
|
826
|
+
break;
|
827
|
+
}
|
828
|
+
if (operation === "create" && col.nullable && count >= 3) {
|
829
|
+
continue;
|
830
|
+
}
|
831
|
+
const value = generateExampleValue(col);
|
832
|
+
fields.push(` ${col.name}: ${value}`);
|
833
|
+
count++;
|
834
|
+
}
|
835
|
+
return fields.join(`,
|
836
|
+
`);
|
837
|
+
}
|
838
|
+
function generateExampleValue(column) {
|
839
|
+
const name = column.name.toLowerCase();
|
840
|
+
if (name.includes("email"))
|
841
|
+
return `'user@example.com'`;
|
842
|
+
if (name.includes("name"))
|
843
|
+
return `'John Doe'`;
|
844
|
+
if (name.includes("title"))
|
845
|
+
return `'Example Title'`;
|
846
|
+
if (name.includes("description"))
|
847
|
+
return `'Example description'`;
|
848
|
+
if (name.includes("phone"))
|
849
|
+
return `'+1234567890'`;
|
850
|
+
if (name.includes("url"))
|
851
|
+
return `'https://example.com'`;
|
852
|
+
if (name.includes("price") || name.includes("amount"))
|
853
|
+
return `99.99`;
|
854
|
+
if (name.includes("quantity") || name.includes("count"))
|
855
|
+
return `10`;
|
856
|
+
if (name.includes("status"))
|
857
|
+
return `'active'`;
|
858
|
+
if (name.includes("_id"))
|
859
|
+
return `'related-id-123'`;
|
860
|
+
switch (column.pgType) {
|
861
|
+
case "boolean":
|
862
|
+
case "bool":
|
863
|
+
return "true";
|
864
|
+
case "int":
|
865
|
+
case "integer":
|
866
|
+
case "smallint":
|
867
|
+
case "bigint":
|
868
|
+
return "42";
|
869
|
+
case "decimal":
|
870
|
+
case "numeric":
|
871
|
+
case "real":
|
872
|
+
case "double precision":
|
873
|
+
case "float":
|
874
|
+
return "123.45";
|
875
|
+
case "date":
|
876
|
+
return `'2024-01-01'`;
|
877
|
+
case "timestamp":
|
878
|
+
case "timestamptz":
|
879
|
+
return `'2024-01-01T00:00:00Z'`;
|
880
|
+
case "json":
|
881
|
+
case "jsonb":
|
882
|
+
return `{ key: 'value' }`;
|
883
|
+
case "uuid":
|
884
|
+
return `'123e4567-e89b-12d3-a456-426614174000'`;
|
885
|
+
default:
|
886
|
+
return `'example value'`;
|
887
|
+
}
|
888
|
+
}
|
889
|
+
function generateQueryParams(table) {
|
890
|
+
const params = {
|
891
|
+
limit: "number - Max records to return (default: 50)",
|
892
|
+
offset: "number - Records to skip",
|
893
|
+
order_by: "string - Field to sort by",
|
894
|
+
order_dir: "'asc' | 'desc' - Sort direction",
|
895
|
+
include: "string - Related resources to include"
|
896
|
+
};
|
897
|
+
let filterCount = 0;
|
898
|
+
for (const col of table.columns) {
|
899
|
+
if (filterCount >= 3)
|
900
|
+
break;
|
901
|
+
const type = postgresTypeToJsonType2(col.pgType);
|
902
|
+
params[col.name] = `${type} - Filter by ${col.name}`;
|
903
|
+
if (type === "string") {
|
904
|
+
params[`${col.name}_like`] = `string - Search in ${col.name}`;
|
905
|
+
} else if (type === "number" || type === "date/datetime") {
|
906
|
+
params[`${col.name}_gt`] = `${type} - Greater than`;
|
907
|
+
params[`${col.name}_lt`] = `${type} - Less than`;
|
908
|
+
}
|
909
|
+
filterCount++;
|
910
|
+
}
|
911
|
+
params["..."] = "Additional filters for all fields";
|
912
|
+
return params;
|
913
|
+
}
|
914
|
+
function postgresTypeToJsonType2(pgType) {
|
915
|
+
switch (pgType) {
|
916
|
+
case "int":
|
917
|
+
case "integer":
|
918
|
+
case "smallint":
|
919
|
+
case "bigint":
|
920
|
+
case "decimal":
|
921
|
+
case "numeric":
|
922
|
+
case "real":
|
923
|
+
case "double precision":
|
924
|
+
case "float":
|
925
|
+
return "number";
|
926
|
+
case "boolean":
|
927
|
+
case "bool":
|
928
|
+
return "boolean";
|
929
|
+
case "date":
|
930
|
+
case "timestamp":
|
931
|
+
case "timestamptz":
|
932
|
+
return "date/datetime";
|
933
|
+
case "json":
|
934
|
+
case "jsonb":
|
935
|
+
return "object";
|
936
|
+
case "uuid":
|
937
|
+
return "uuid";
|
938
|
+
case "text[]":
|
939
|
+
case "varchar[]":
|
940
|
+
return "string[]";
|
941
|
+
case "int[]":
|
942
|
+
case "integer[]":
|
943
|
+
return "number[]";
|
944
|
+
default:
|
945
|
+
return "string";
|
946
|
+
}
|
947
|
+
}
|
948
|
+
function generateFieldDescription2(column, table) {
|
949
|
+
const descriptions = [];
|
950
|
+
if (column.name === "id") {
|
951
|
+
descriptions.push("Primary key");
|
952
|
+
} else if (column.name === "created_at") {
|
953
|
+
descriptions.push("Creation timestamp");
|
954
|
+
} else if (column.name === "updated_at") {
|
955
|
+
descriptions.push("Last update timestamp");
|
956
|
+
} else if (column.name === "deleted_at") {
|
957
|
+
descriptions.push("Soft delete timestamp");
|
958
|
+
} else if (column.name.endsWith("_id")) {
|
959
|
+
const relatedTable = column.name.slice(0, -3);
|
960
|
+
descriptions.push(`Foreign key to ${relatedTable}`);
|
961
|
+
} else {
|
962
|
+
descriptions.push(column.name.replace(/_/g, " "));
|
963
|
+
}
|
964
|
+
return descriptions.join(", ");
|
965
|
+
}
|
966
|
+
function generateUnifiedContractMarkdown(contract) {
|
967
|
+
const lines = [];
|
968
|
+
lines.push("# API & SDK Contract");
|
969
|
+
lines.push("");
|
970
|
+
lines.push(contract.description);
|
971
|
+
lines.push("");
|
972
|
+
lines.push(`**Version:** ${contract.version}`);
|
973
|
+
lines.push(`**Generated:** ${new Date(contract.generatedAt).toLocaleString()}`);
|
974
|
+
lines.push("");
|
975
|
+
lines.push("## SDK Setup");
|
976
|
+
lines.push("");
|
977
|
+
lines.push("### Installation");
|
978
|
+
lines.push("");
|
979
|
+
lines.push("```bash");
|
980
|
+
lines.push("# The SDK is generated in the client/ directory");
|
981
|
+
lines.push("# Import it directly from your generated code");
|
982
|
+
lines.push("```");
|
983
|
+
lines.push("");
|
984
|
+
lines.push("### Initialization");
|
985
|
+
lines.push("");
|
986
|
+
for (const example of contract.sdk.initialization) {
|
987
|
+
lines.push(`**${example.description}:**`);
|
988
|
+
lines.push("");
|
989
|
+
lines.push("```typescript");
|
990
|
+
lines.push(example.code);
|
991
|
+
lines.push("```");
|
992
|
+
lines.push("");
|
993
|
+
}
|
994
|
+
if (contract.sdk.authentication.length > 0) {
|
995
|
+
lines.push("### Authentication");
|
996
|
+
lines.push("");
|
997
|
+
for (const auth of contract.sdk.authentication) {
|
998
|
+
lines.push(`**${auth.description}:**`);
|
999
|
+
lines.push("");
|
1000
|
+
lines.push("```typescript");
|
1001
|
+
lines.push(auth.code);
|
1002
|
+
lines.push("```");
|
1003
|
+
lines.push("");
|
1004
|
+
}
|
1005
|
+
}
|
1006
|
+
lines.push("## Resources");
|
1007
|
+
lines.push("");
|
1008
|
+
for (const resource of contract.resources) {
|
1009
|
+
lines.push(`### ${resource.name}`);
|
1010
|
+
lines.push("");
|
1011
|
+
lines.push(resource.description);
|
1012
|
+
lines.push("");
|
1013
|
+
lines.push("#### SDK Methods");
|
1014
|
+
lines.push("");
|
1015
|
+
lines.push(`Access via: \`${resource.sdk.client}\``);
|
1016
|
+
lines.push("");
|
1017
|
+
for (const method of resource.sdk.methods) {
|
1018
|
+
lines.push(`**${method.name}**`);
|
1019
|
+
lines.push(`- Signature: \`${method.signature}\``);
|
1020
|
+
lines.push(`- ${method.description}`);
|
1021
|
+
if (method.correspondsTo) {
|
1022
|
+
lines.push(`- API: \`${method.correspondsTo}\``);
|
1023
|
+
}
|
1024
|
+
lines.push("");
|
1025
|
+
lines.push("```typescript");
|
1026
|
+
lines.push(method.example);
|
1027
|
+
lines.push("```");
|
1028
|
+
lines.push("");
|
1029
|
+
}
|
1030
|
+
lines.push("#### API Endpoints");
|
1031
|
+
lines.push("");
|
1032
|
+
for (const endpoint of resource.api.endpoints) {
|
1033
|
+
lines.push(`- \`${endpoint.method} ${endpoint.path}\``);
|
1034
|
+
lines.push(` - ${endpoint.description}`);
|
1035
|
+
if (endpoint.requestBody) {
|
1036
|
+
lines.push(` - Request: \`${endpoint.requestBody}\``);
|
1037
|
+
}
|
1038
|
+
if (endpoint.responseBody) {
|
1039
|
+
lines.push(` - Response: \`${endpoint.responseBody}\``);
|
1040
|
+
}
|
1041
|
+
}
|
1042
|
+
lines.push("");
|
1043
|
+
lines.push("#### Fields");
|
1044
|
+
lines.push("");
|
1045
|
+
lines.push("| Field | Type | TypeScript | Required | Description |");
|
1046
|
+
lines.push("|-------|------|------------|----------|-------------|");
|
1047
|
+
for (const field of resource.fields) {
|
1048
|
+
const required = field.required ? "✓" : "";
|
1049
|
+
const fk = field.foreignKey ? ` → ${field.foreignKey.table}` : "";
|
1050
|
+
lines.push(`| ${field.name} | ${field.type} | \`${field.tsType}\` | ${required} | ${field.description}${fk} |`);
|
1051
|
+
}
|
1052
|
+
lines.push("");
|
1053
|
+
}
|
1054
|
+
if (contract.relationships.length > 0) {
|
1055
|
+
lines.push("## Relationships");
|
1056
|
+
lines.push("");
|
1057
|
+
for (const rel of contract.relationships) {
|
1058
|
+
lines.push(`- **${rel.from}** → **${rel.to}** (${rel.type}): ${rel.description}`);
|
1059
|
+
}
|
1060
|
+
lines.push("");
|
1061
|
+
}
|
1062
|
+
lines.push("## Type Imports");
|
1063
|
+
lines.push("");
|
1064
|
+
lines.push("```typescript");
|
1065
|
+
lines.push("// Import SDK and types");
|
1066
|
+
lines.push("import { SDK } from './client';");
|
1067
|
+
lines.push("");
|
1068
|
+
lines.push("// Import types for a specific table");
|
1069
|
+
lines.push("import type {");
|
1070
|
+
lines.push(" SelectTableName, // Full record type");
|
1071
|
+
lines.push(" InsertTableName, // Create payload type");
|
1072
|
+
lines.push(" UpdateTableName // Update payload type");
|
1073
|
+
lines.push("} from './client/types/table_name';");
|
1074
|
+
lines.push("");
|
1075
|
+
lines.push("// Import all types");
|
1076
|
+
lines.push("import type * as Types from './client/types';");
|
1077
|
+
lines.push("```");
|
1078
|
+
lines.push("");
|
1079
|
+
return lines.join(`
|
1080
|
+
`);
|
1081
|
+
}
|
1082
|
+
function emitUnifiedContract(model, config) {
|
1083
|
+
const contract = generateUnifiedContract(model, config);
|
1084
|
+
const contractJson = JSON.stringify(contract, null, 2);
|
1085
|
+
return `/**
|
1086
|
+
* Unified API & SDK Contract
|
1087
|
+
*
|
1088
|
+
* This module exports a comprehensive contract that describes both
|
1089
|
+
* API endpoints and SDK usage for all resources.
|
1090
|
+
*
|
1091
|
+
* Use this as your primary reference for:
|
1092
|
+
* - SDK initialization and authentication
|
1093
|
+
* - Available methods and their signatures
|
1094
|
+
* - API endpoints and parameters
|
1095
|
+
* - Type definitions and relationships
|
1096
|
+
*/
|
1097
|
+
|
1098
|
+
export const contract = ${contractJson};
|
1099
|
+
|
1100
|
+
export const contractMarkdown = \`${generateUnifiedContractMarkdown(contract).replace(/`/g, "\\`")}\`;
|
1101
|
+
|
1102
|
+
/**
|
1103
|
+
* Get the contract in different formats
|
1104
|
+
*/
|
1105
|
+
export function getContract(format: 'json' | 'markdown' = 'json') {
|
1106
|
+
if (format === 'markdown') {
|
1107
|
+
return contractMarkdown;
|
1108
|
+
}
|
1109
|
+
return contract;
|
1110
|
+
}
|
1111
|
+
|
1112
|
+
/**
|
1113
|
+
* Quick reference for all SDK clients
|
1114
|
+
*/
|
1115
|
+
export const sdkClients = ${JSON.stringify(contract.resources.map((r) => ({
|
1116
|
+
name: r.tableName,
|
1117
|
+
client: r.sdk.client,
|
1118
|
+
methods: r.sdk.methods.map((m) => m.name)
|
1119
|
+
})), null, 2)};
|
1120
|
+
|
1121
|
+
/**
|
1122
|
+
* Type export reference
|
1123
|
+
*/
|
1124
|
+
export const typeImports = \`
|
1125
|
+
// Import the SDK
|
1126
|
+
import { SDK } from './client';
|
1127
|
+
|
1128
|
+
// Import types for a specific resource
|
1129
|
+
${contract.resources.slice(0, 1).map((r) => `import type { Select${r.name}, Insert${r.name}, Update${r.name} } from './client/types/${r.tableName}';`).join(`
|
1130
|
+
`)}
|
1131
|
+
|
1132
|
+
// Import all types
|
1133
|
+
import type * as Types from './client/types';
|
1134
|
+
\`;
|
1135
|
+
`;
|
1136
|
+
}
|
1137
|
+
var init_emit_sdk_contract = __esm(() => {
|
1138
|
+
init_utils();
|
1139
|
+
});
|
1140
|
+
|
472
1141
|
// src/index.ts
|
473
1142
|
var import_config = __toESM(require_config(), 1);
|
474
1143
|
import { join, relative } from "node:path";
|
@@ -715,22 +1384,8 @@ export const buildWithFor = (t: TableName) =>
|
|
715
1384
|
`;
|
716
1385
|
}
|
717
1386
|
|
718
|
-
// src/utils.ts
|
719
|
-
import { mkdir, writeFile } from "fs/promises";
|
720
|
-
import { dirname } from "path";
|
721
|
-
var pascal = (s) => s.split(/[_\s-]+/).map((w) => w?.[0] ? w[0].toUpperCase() + w.slice(1) : "").join("");
|
722
|
-
async function writeFiles(files) {
|
723
|
-
for (const f of files) {
|
724
|
-
await mkdir(dirname(f.path), { recursive: true });
|
725
|
-
await writeFile(f.path, f.content, "utf-8");
|
726
|
-
}
|
727
|
-
}
|
728
|
-
async function ensureDirs(dirs) {
|
729
|
-
for (const d of dirs)
|
730
|
-
await mkdir(d, { recursive: true });
|
731
|
-
}
|
732
|
-
|
733
1387
|
// src/emit-zod.ts
|
1388
|
+
init_utils();
|
734
1389
|
function emitZod(table, opts) {
|
735
1390
|
const Type = pascal(table.name);
|
736
1391
|
const zFor = (pg) => {
|
@@ -774,6 +1429,7 @@ export type Update${Type} = z.infer<typeof Update${Type}Schema>;
|
|
774
1429
|
}
|
775
1430
|
|
776
1431
|
// src/emit-routes-hono.ts
|
1432
|
+
init_utils();
|
777
1433
|
function emitHonoRoutes(table, _graph, opts) {
|
778
1434
|
const fileTableName = table.name;
|
779
1435
|
const Type = pascal(table.name);
|
@@ -934,6 +1590,7 @@ ${hasAuth ? `
|
|
934
1590
|
}
|
935
1591
|
|
936
1592
|
// src/emit-client.ts
|
1593
|
+
init_utils();
|
937
1594
|
function emitClient(table, useJsExtensions) {
|
938
1595
|
const Type = pascal(table.name);
|
939
1596
|
const ext = useJsExtensions ? ".js" : "";
|
@@ -1716,6 +2373,7 @@ export async function authMiddleware(c: Context, next: Next) {
|
|
1716
2373
|
}
|
1717
2374
|
|
1718
2375
|
// src/emit-router-hono.ts
|
2376
|
+
init_utils();
|
1719
2377
|
function emitHonoRouter(tables, hasAuth, useJsExtensions) {
|
1720
2378
|
const tableNames = tables.map((t) => t.name).sort();
|
1721
2379
|
const ext = useJsExtensions ? ".js" : "";
|
@@ -2095,6 +2753,7 @@ export async function deleteRecord(
|
|
2095
2753
|
}
|
2096
2754
|
|
2097
2755
|
// src/emit-tests.ts
|
2756
|
+
init_utils();
|
2098
2757
|
function emitTableTest(table, model, clientPath, framework = "vitest") {
|
2099
2758
|
const Type = pascal(table.name);
|
2100
2759
|
const tableName = table.name;
|
@@ -2312,6 +2971,14 @@ set -e
|
|
2312
2971
|
|
2313
2972
|
SCRIPT_DIR="$( cd "$( dirname "\${BASH_SOURCE[0]}" )" && pwd )"
|
2314
2973
|
|
2974
|
+
# PROJECT_ROOT is the root of your project (3 levels up from tests directory)
|
2975
|
+
# Use this to reference files from your project root
|
2976
|
+
# Example: $PROJECT_ROOT/src/server.ts
|
2977
|
+
PROJECT_ROOT="$( cd "$SCRIPT_DIR/../../.." && pwd )"
|
2978
|
+
|
2979
|
+
echo "\uD83D\uDCCD Project root: $PROJECT_ROOT"
|
2980
|
+
echo "\uD83D\uDCCD Test directory: $SCRIPT_DIR"
|
2981
|
+
|
2315
2982
|
# Cleanup function to ensure database is stopped
|
2316
2983
|
cleanup() {
|
2317
2984
|
echo ""
|
@@ -2356,25 +3023,99 @@ export TEST_API_URL="http://localhost:3000"
|
|
2356
3023
|
echo "⏳ Waiting for database..."
|
2357
3024
|
sleep 3
|
2358
3025
|
|
2359
|
-
#
|
2360
|
-
|
2361
|
-
|
2362
|
-
|
3026
|
+
# REQUIRED: Run migrations on the test database
|
3027
|
+
echo ""
|
3028
|
+
echo "\uD83D\uDCCA Database Migration Step"
|
3029
|
+
echo "========================================="
|
3030
|
+
echo ""
|
3031
|
+
echo "⚠️ IMPORTANT: You must run migrations before tests can work!"
|
3032
|
+
echo ""
|
3033
|
+
echo "Choose one of the following options:"
|
3034
|
+
echo ""
|
3035
|
+
echo "Option 1: Add your migration command (recommended):"
|
3036
|
+
echo " Uncomment and modify one of these examples:"
|
3037
|
+
echo ""
|
3038
|
+
echo " # For Prisma:"
|
3039
|
+
echo " # npx prisma migrate deploy"
|
3040
|
+
echo ""
|
3041
|
+
echo " # For Drizzle:"
|
3042
|
+
echo " # npx drizzle-kit push --config=./drizzle.config.ts"
|
3043
|
+
echo ""
|
3044
|
+
echo " # For node-pg-migrate:"
|
3045
|
+
echo " # npm run migrate up"
|
3046
|
+
echo ""
|
3047
|
+
echo " # For Knex:"
|
3048
|
+
echo " # npx knex migrate:latest"
|
3049
|
+
echo ""
|
3050
|
+
echo " # For TypeORM:"
|
3051
|
+
echo " # npm run typeorm migration:run"
|
3052
|
+
echo ""
|
3053
|
+
echo " # For custom migration scripts:"
|
3054
|
+
echo " # node ./scripts/migrate.js"
|
3055
|
+
echo ""
|
3056
|
+
echo "Option 2: Skip migrations (only if your database is already set up):"
|
3057
|
+
echo " Uncomment the line: # SKIP_MIGRATIONS=true"
|
3058
|
+
echo ""
|
3059
|
+
echo "========================================="
|
3060
|
+
echo ""
|
3061
|
+
|
3062
|
+
# MIGRATION_COMMAND:
|
3063
|
+
# Add your migration command here. Examples using PROJECT_ROOT:
|
3064
|
+
# MIGRATION_COMMAND="cd $PROJECT_ROOT && npx prisma migrate deploy"
|
3065
|
+
# MIGRATION_COMMAND="cd $PROJECT_ROOT && npx drizzle-kit push --config=./drizzle.config.ts"
|
3066
|
+
# MIGRATION_COMMAND="cd $PROJECT_ROOT && npm run migrate up"
|
3067
|
+
# MIGRATION_COMMAND="DATABASE_URL=$TEST_DATABASE_URL node $PROJECT_ROOT/scripts/migrate.js"
|
3068
|
+
|
3069
|
+
# Or skip migrations if your database is pre-configured:
|
3070
|
+
# SKIP_MIGRATIONS=true
|
2363
3071
|
|
3072
|
+
if [ -z "\${MIGRATION_COMMAND}" ] && [ -z "\${SKIP_MIGRATIONS}" ]; then
|
3073
|
+
echo "❌ ERROR: No migration strategy configured!"
|
3074
|
+
echo ""
|
3075
|
+
echo " Please edit this script and either:"
|
3076
|
+
echo " 1. Set MIGRATION_COMMAND with your migration command"
|
3077
|
+
echo " 2. Set SKIP_MIGRATIONS=true if migrations aren't needed"
|
3078
|
+
echo ""
|
3079
|
+
echo " Tests cannot run without a properly migrated database schema."
|
3080
|
+
echo ""
|
3081
|
+
exit 1
|
3082
|
+
fi
|
3083
|
+
|
3084
|
+
if [ ! -z "\${MIGRATION_COMMAND}" ]; then
|
3085
|
+
echo "\uD83D\uDCCA Running migrations..."
|
3086
|
+
echo " Command: \${MIGRATION_COMMAND}"
|
3087
|
+
eval "\${MIGRATION_COMMAND}"
|
3088
|
+
if [ $? -ne 0 ]; then
|
3089
|
+
echo "❌ Migration failed! Please check your migration command and database connection."
|
3090
|
+
exit 1
|
3091
|
+
fi
|
3092
|
+
echo "✅ Migrations completed successfully"
|
3093
|
+
elif [ "\${SKIP_MIGRATIONS}" = "true" ]; then
|
3094
|
+
echo "⏭️ Skipping migrations (SKIP_MIGRATIONS=true)"
|
3095
|
+
else
|
3096
|
+
echo "❌ Invalid migration configuration"
|
3097
|
+
exit 1
|
3098
|
+
fi
|
3099
|
+
|
3100
|
+
echo ""
|
2364
3101
|
echo "\uD83D\uDE80 Starting API server..."
|
2365
3102
|
echo "⚠️ TODO: Uncomment and customize the API server startup command below:"
|
2366
3103
|
echo ""
|
2367
|
-
echo " # Example for Node.js/Bun:"
|
2368
|
-
echo " # cd
|
3104
|
+
echo " # Example for Node.js/Bun using PROJECT_ROOT:"
|
3105
|
+
echo " # cd \\$PROJECT_ROOT && npm run dev &"
|
2369
3106
|
echo " # SERVER_PID=\\$!"
|
2370
3107
|
echo ""
|
2371
3108
|
echo " # Example for custom server file:"
|
2372
|
-
echo " #
|
3109
|
+
echo " # DATABASE_URL=\\$TEST_DATABASE_URL node \\$PROJECT_ROOT/src/server.js &"
|
3110
|
+
echo " # SERVER_PID=\\$!"
|
3111
|
+
echo ""
|
3112
|
+
echo " # Example for Bun:"
|
3113
|
+
echo " # DATABASE_URL=\\$TEST_DATABASE_URL bun \\$PROJECT_ROOT/src/index.ts &"
|
2373
3114
|
echo " # SERVER_PID=\\$!"
|
2374
3115
|
echo ""
|
2375
3116
|
echo " Please edit this script to start your API server."
|
2376
3117
|
echo ""
|
2377
|
-
# cd
|
3118
|
+
# cd $PROJECT_ROOT && npm run dev &
|
2378
3119
|
# SERVER_PID=$!
|
2379
3120
|
# sleep 3
|
2380
3121
|
|
@@ -2734,10 +3475,12 @@ function generateTestCases(table, sampleData, updateData, hasForeignKeys = false
|
|
2734
3475
|
}
|
2735
3476
|
|
2736
3477
|
// src/emit-api-contract.ts
|
3478
|
+
init_utils();
|
2737
3479
|
function generateApiContract(model, config) {
|
2738
3480
|
const resources = [];
|
2739
3481
|
const relationships = [];
|
2740
|
-
|
3482
|
+
const tables = Object.values(model.tables || {});
|
3483
|
+
for (const table of tables) {
|
2741
3484
|
resources.push(generateResourceContract(table, model));
|
2742
3485
|
for (const fk of table.fks) {
|
2743
3486
|
relationships.push({
|
@@ -3007,6 +3750,10 @@ export function getApiContract(format: 'json' | 'markdown' = 'json') {
|
|
3007
3750
|
`;
|
3008
3751
|
}
|
3009
3752
|
|
3753
|
+
// src/index.ts
|
3754
|
+
init_emit_sdk_contract();
|
3755
|
+
init_utils();
|
3756
|
+
|
3010
3757
|
// src/types.ts
|
3011
3758
|
function normalizeAuthConfig(input) {
|
3012
3759
|
if (!input)
|
@@ -3104,6 +3851,9 @@ async function generate(configPath) {
|
|
3104
3851
|
path: join(serverDir, "core", "operations.ts"),
|
3105
3852
|
content: emitCoreOperations()
|
3106
3853
|
});
|
3854
|
+
if (process.env.SDK_DEBUG) {
|
3855
|
+
console.log(`[Index] About to process ${Object.keys(model.tables || {}).length} tables for generation`);
|
3856
|
+
}
|
3107
3857
|
for (const table of Object.values(model.tables)) {
|
3108
3858
|
const typesSrc = emitTypes(table, { numericMode: "string" });
|
3109
3859
|
files.push({ path: join(serverDir, "types", `${table.name}.ts`), content: typesSrc });
|
@@ -3153,6 +3903,20 @@ async function generate(configPath) {
|
|
3153
3903
|
path: join(serverDir, "api-contract.ts"),
|
3154
3904
|
content: emitApiContract(model, cfg)
|
3155
3905
|
});
|
3906
|
+
const contractCode = emitUnifiedContract(model, cfg);
|
3907
|
+
files.push({
|
3908
|
+
path: join(serverDir, "contract.ts"),
|
3909
|
+
content: contractCode
|
3910
|
+
});
|
3911
|
+
const { generateUnifiedContract: generateUnifiedContract2, generateUnifiedContractMarkdown: generateUnifiedContractMarkdown2 } = await Promise.resolve().then(() => (init_emit_sdk_contract(), exports_emit_sdk_contract));
|
3912
|
+
if (process.env.SDK_DEBUG) {
|
3913
|
+
console.log(`[Index] Model has ${Object.keys(model.tables || {}).length} tables before contract generation`);
|
3914
|
+
}
|
3915
|
+
const contract = generateUnifiedContract2(model, cfg);
|
3916
|
+
files.push({
|
3917
|
+
path: join(serverDir, "CONTRACT.md"),
|
3918
|
+
content: generateUnifiedContractMarkdown2(contract)
|
3919
|
+
});
|
3156
3920
|
if (generateTests) {
|
3157
3921
|
console.log("\uD83E\uDDEA Generating tests...");
|
3158
3922
|
const relativeClientPath = relative(testDir, clientDir);
|