@topogram/starter-web-api-db 0.1.7
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 +32 -0
- package/implementation/README.md +5 -0
- package/implementation/backend/reference.js +49 -0
- package/implementation/backend/repository-reference.js +30 -0
- package/implementation/backend/repository-renderers.js +108 -0
- package/implementation/index.js +53 -0
- package/implementation/runtime/check-renderers.js +72 -0
- package/implementation/runtime/checks.js +70 -0
- package/implementation/runtime/reference.js +73 -0
- package/implementation/web/reference.js +45 -0
- package/implementation/web/renderers.js +45 -0
- package/implementation/web/screens-reference.js +7 -0
- package/package.json +24 -0
- package/topogram/actors/actor-operator.tg +6 -0
- package/topogram/capabilities/cap-create-greeting.tg +11 -0
- package/topogram/capabilities/cap-get-greeting.tg +11 -0
- package/topogram/capabilities/cap-list-greetings.tg +11 -0
- package/topogram/capabilities/cap-update-greeting.tg +12 -0
- package/topogram/entities/entity-greeting.tg +21 -0
- package/topogram/projections/proj-api.tg +29 -0
- package/topogram/projections/proj-db-postgres.tg +31 -0
- package/topogram/projections/proj-ui-shared.tg +38 -0
- package/topogram/projections/proj-ui-web.tg +34 -0
- package/topogram/shapes/shape-input-create-greeting.tg +7 -0
- package/topogram/shapes/shape-input-get-greeting.tg +10 -0
- package/topogram/shapes/shape-input-list-greetings.tg +11 -0
- package/topogram/shapes/shape-input-update-greeting.tg +11 -0
- package/topogram/shapes/shape-output-greeting-card.tg +7 -0
- package/topogram/shapes/shape-output-greeting-detail.tg +7 -0
- package/topogram/verifications/verification-runtime-flow.tg +16 -0
- package/topogram/verifications/verification-runtime-smoke.tg +16 -0
- package/topogram-template.json +8 -0
- package/topogram.project.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Topogram Starter: Web API DB
|
|
2
|
+
|
|
3
|
+
Package-backed SvelteKit + Hono + Postgres starter for `topogram new`.
|
|
4
|
+
|
|
5
|
+
This starter includes executable `implementation/` provider code. Review local
|
|
6
|
+
template trust before generation.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
topogram new ./web-api-db --template @topogram/starter-web-api-db
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Contract
|
|
13
|
+
|
|
14
|
+
- Catalog id: `web-api-db`
|
|
15
|
+
- Surfaces: `web`, `api`, `database`
|
|
16
|
+
- Generators: `@topogram/generator-sveltekit-web`, `@topogram/generator-hono-api`, `@topogram/generator-postgres-db`
|
|
17
|
+
- Runtime stack: SvelteKit + Hono + Postgres
|
|
18
|
+
- Executable implementation: yes
|
|
19
|
+
- Purpose: full-stack starter with explicit web-to-API and API-to-database wiring.
|
|
20
|
+
|
|
21
|
+
## First Run
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
topogram new ./web-api-db --template web-api-db
|
|
25
|
+
cd ./web-api-db
|
|
26
|
+
npm install
|
|
27
|
+
npm run doctor
|
|
28
|
+
npm run check
|
|
29
|
+
npm run generate
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Review copied `implementation/` code before refreshing trust after local edits.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const HELLO_BACKEND_REFERENCE = {
|
|
2
|
+
serviceName: "topogram-starter-server",
|
|
3
|
+
renderSeedScript() {
|
|
4
|
+
const reference = HELLO_BACKEND_REFERENCE;
|
|
5
|
+
return `import { PrismaClient } from "@prisma/client";
|
|
6
|
+
|
|
7
|
+
const prisma = new PrismaClient();
|
|
8
|
+
|
|
9
|
+
const demoGreetingId = process.env.TOPOGRAM_DEMO_PRIMARY_ID || "${reference.demo.greetingId}";
|
|
10
|
+
const demoMessage = process.env.TOPOGRAM_DEMO_MESSAGE || "${reference.demo.message}";
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
const now = new Date();
|
|
14
|
+
|
|
15
|
+
await prisma.greeting.upsert({
|
|
16
|
+
where: { id: demoGreetingId },
|
|
17
|
+
update: {
|
|
18
|
+
message: demoMessage,
|
|
19
|
+
created_at: now
|
|
20
|
+
},
|
|
21
|
+
create: {
|
|
22
|
+
id: demoGreetingId,
|
|
23
|
+
message: demoMessage,
|
|
24
|
+
created_at: now
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
console.log(JSON.stringify({
|
|
29
|
+
ok: true,
|
|
30
|
+
demoGreetingId,
|
|
31
|
+
seededGreetingCount: 1
|
|
32
|
+
}, null, 2));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
main()
|
|
36
|
+
.catch((error) => {
|
|
37
|
+
console.error(error);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
})
|
|
40
|
+
.finally(async () => {
|
|
41
|
+
await prisma.$disconnect();
|
|
42
|
+
});
|
|
43
|
+
`;
|
|
44
|
+
},
|
|
45
|
+
demo: {
|
|
46
|
+
greetingId: "33333333-3333-4333-8333-333333333333",
|
|
47
|
+
message: "hello-from-topogram"
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const HELLO_BACKEND_REPOSITORY_REFERENCE = {
|
|
2
|
+
capabilityIds: [
|
|
3
|
+
"cap_get_greeting",
|
|
4
|
+
"cap_list_greetings",
|
|
5
|
+
"cap_create_greeting",
|
|
6
|
+
"cap_update_greeting"
|
|
7
|
+
],
|
|
8
|
+
preconditionCapabilityIds: [],
|
|
9
|
+
preconditionResource: {
|
|
10
|
+
variableName: "currentGreeting",
|
|
11
|
+
repositoryMethod: "getGreeting",
|
|
12
|
+
inputField: "greeting_id",
|
|
13
|
+
versionField: "created_at"
|
|
14
|
+
},
|
|
15
|
+
downloadCapabilityId: "",
|
|
16
|
+
repositoryInterfaceName: "StarterRepository",
|
|
17
|
+
prismaRepositoryClassName: "PrismaStarterRepository",
|
|
18
|
+
drizzleRepositoryClassName: "DrizzleStarterRepository",
|
|
19
|
+
dependencyName: "starterRepository",
|
|
20
|
+
lookupBindings: [],
|
|
21
|
+
export: {
|
|
22
|
+
filename: "starter-export.json",
|
|
23
|
+
contentType: "application/json"
|
|
24
|
+
},
|
|
25
|
+
drizzleHint: "Fill in Drizzle query logic if you switch this starter to a Drizzle runtime.",
|
|
26
|
+
drizzleSchemaImports: ["greetingsTable"],
|
|
27
|
+
additionalTypeNames: [],
|
|
28
|
+
additionalTypeDeclarations: [],
|
|
29
|
+
additionalInterfaceMethods: []
|
|
30
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export function renderHelloPrismaRepositoryBody({
|
|
2
|
+
repositoryInterfaceName,
|
|
3
|
+
prismaRepositoryClassName
|
|
4
|
+
}) {
|
|
5
|
+
return `
|
|
6
|
+
function iso(value: Date | string | null | undefined): string | undefined {
|
|
7
|
+
if (!value) return undefined;
|
|
8
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function nextCursor<T extends { created_at: Date | string }>(items: T[]): string {
|
|
12
|
+
return items.length > 0 ? iso(items[items.length - 1]!.created_at) || "" : "";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function mapGreetingRecord(greeting: {
|
|
16
|
+
id: string;
|
|
17
|
+
message: string;
|
|
18
|
+
created_at: Date | string;
|
|
19
|
+
}): GetGreetingResult {
|
|
20
|
+
return {
|
|
21
|
+
id: greeting.id,
|
|
22
|
+
message: greeting.message,
|
|
23
|
+
created_at: iso(greeting.created_at)!
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class ${prismaRepositoryClassName} implements ${repositoryInterfaceName} {
|
|
28
|
+
constructor(private readonly prisma: PrismaClient) {}
|
|
29
|
+
|
|
30
|
+
async getGreeting(input: GetGreetingInput): Promise<GetGreetingResult> {
|
|
31
|
+
const greeting = await this.prisma.greeting.findUnique({ where: { id: input.greeting_id } });
|
|
32
|
+
if (!greeting) throw new HttpError(404, "cap_get_greeting_not_found", "Greeting not found");
|
|
33
|
+
return mapGreetingRecord(greeting);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async listGreetings(input: ListGreetingsInput): Promise<ListGreetingsResult> {
|
|
37
|
+
const take = Math.min(input.limit ?? 25, 100);
|
|
38
|
+
const greetings = await this.prisma.greeting.findMany({
|
|
39
|
+
where: { ...(input.after ? { created_at: { lt: new Date(input.after) } } : {}) },
|
|
40
|
+
orderBy: [{ created_at: "desc" }],
|
|
41
|
+
take: take + 1
|
|
42
|
+
});
|
|
43
|
+
const page = greetings.slice(0, take).map(mapGreetingRecord);
|
|
44
|
+
return {
|
|
45
|
+
items: page,
|
|
46
|
+
next_cursor: nextCursor(greetings.slice(0, take))
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async createGreeting(input: CreateGreetingInput): Promise<CreateGreetingResult> {
|
|
51
|
+
const greeting = await this.prisma.greeting.create({
|
|
52
|
+
data: {
|
|
53
|
+
id: crypto.randomUUID(),
|
|
54
|
+
message: input.message,
|
|
55
|
+
created_at: new Date()
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return mapGreetingRecord(greeting);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async updateGreeting(input: UpdateGreetingInput): Promise<UpdateGreetingResult> {
|
|
62
|
+
const greeting = await this.prisma.greeting.update({
|
|
63
|
+
where: { id: input.greeting_id },
|
|
64
|
+
data: {
|
|
65
|
+
...(input.message !== undefined ? { message: input.message } : {})
|
|
66
|
+
}
|
|
67
|
+
}).catch((error) => {
|
|
68
|
+
throw new HttpError(404, "cap_get_greeting_not_found", error instanceof Error ? error.message : "Greeting not found");
|
|
69
|
+
});
|
|
70
|
+
return mapGreetingRecord(greeting);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function renderHelloDrizzleRepositoryBody({
|
|
77
|
+
repositoryInterfaceName,
|
|
78
|
+
drizzleRepositoryClassName,
|
|
79
|
+
drizzleHint
|
|
80
|
+
}) {
|
|
81
|
+
return `
|
|
82
|
+
export class ${drizzleRepositoryClassName} implements ${repositoryInterfaceName} {
|
|
83
|
+
constructor(private readonly db: NodePgDatabase) {}
|
|
84
|
+
|
|
85
|
+
private unsupported(): never {
|
|
86
|
+
void this.db;
|
|
87
|
+
void greetingsTable;
|
|
88
|
+
throw new Error(${JSON.stringify(drizzleHint)});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async getGreeting(): Promise<GetGreetingResult> {
|
|
92
|
+
this.unsupported();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async listGreetings(): Promise<ListGreetingsResult> {
|
|
96
|
+
this.unsupported();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async createGreeting(): Promise<CreateGreetingResult> {
|
|
100
|
+
this.unsupported();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async updateGreeting(): Promise<UpdateGreetingResult> {
|
|
104
|
+
this.unsupported();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { HELLO_BACKEND_REFERENCE } from "./backend/reference.js";
|
|
2
|
+
import { HELLO_BACKEND_REPOSITORY_REFERENCE } from "./backend/repository-reference.js";
|
|
3
|
+
import {
|
|
4
|
+
renderHelloDrizzleRepositoryBody,
|
|
5
|
+
renderHelloPrismaRepositoryBody
|
|
6
|
+
} from "./backend/repository-renderers.js";
|
|
7
|
+
import { HELLO_RUNTIME_REFERENCE } from "./runtime/reference.js";
|
|
8
|
+
import { HELLO_RUNTIME_CHECKS } from "./runtime/checks.js";
|
|
9
|
+
import {
|
|
10
|
+
renderHelloRuntimeCheckCases,
|
|
11
|
+
renderHelloRuntimeCheckCreatePayload,
|
|
12
|
+
renderHelloRuntimeCheckHelpers,
|
|
13
|
+
renderHelloRuntimeCheckState
|
|
14
|
+
} from "./runtime/check-renderers.js";
|
|
15
|
+
import { HELLO_WEB_REFERENCE } from "./web/reference.js";
|
|
16
|
+
import { HELLO_WEB_SCREEN_REFERENCE } from "./web/screens-reference.js";
|
|
17
|
+
import {
|
|
18
|
+
renderHelloHomePage,
|
|
19
|
+
renderHelloRoutes
|
|
20
|
+
} from "./web/renderers.js";
|
|
21
|
+
|
|
22
|
+
export const WEB_API_DB_IMPLEMENTATION = {
|
|
23
|
+
exampleId: "web-api-db-template",
|
|
24
|
+
exampleRoot: "/topogram",
|
|
25
|
+
backend: {
|
|
26
|
+
reference: HELLO_BACKEND_REFERENCE,
|
|
27
|
+
repositoryReference: HELLO_BACKEND_REPOSITORY_REFERENCE,
|
|
28
|
+
repositoryRenderers: {
|
|
29
|
+
renderPrismaRepositoryBody: renderHelloPrismaRepositoryBody,
|
|
30
|
+
renderDrizzleRepositoryBody: renderHelloDrizzleRepositoryBody
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
runtime: {
|
|
34
|
+
reference: HELLO_RUNTIME_REFERENCE,
|
|
35
|
+
checks: HELLO_RUNTIME_CHECKS,
|
|
36
|
+
checkRenderers: {
|
|
37
|
+
renderRuntimeCheckState: renderHelloRuntimeCheckState,
|
|
38
|
+
renderRuntimeCheckCreatePayload: renderHelloRuntimeCheckCreatePayload,
|
|
39
|
+
renderRuntimeCheckHelpers: renderHelloRuntimeCheckHelpers,
|
|
40
|
+
renderRuntimeCheckCases: renderHelloRuntimeCheckCases
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
web: {
|
|
44
|
+
reference: HELLO_WEB_REFERENCE,
|
|
45
|
+
screenReference: HELLO_WEB_SCREEN_REFERENCE,
|
|
46
|
+
renderers: {
|
|
47
|
+
renderHomePage: renderHelloHomePage,
|
|
48
|
+
renderRoutes: renderHelloRoutes
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default WEB_API_DB_IMPLEMENTATION;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function renderHelloRuntimeCheckState() {
|
|
2
|
+
return `const state = {
|
|
3
|
+
createdPrimary: null,
|
|
4
|
+
latestPrimary: null
|
|
5
|
+
};`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function renderHelloRuntimeCheckCreatePayload() {
|
|
9
|
+
return `function createPayload(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
message: "Hello from the runtime check",
|
|
12
|
+
...overrides
|
|
13
|
+
};
|
|
14
|
+
}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function renderHelloRuntimeCheckHelpers() {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function renderHelloRuntimeCheckCases() {
|
|
22
|
+
return ` } else if (definition.id === "api_seed_greeting_ready") {
|
|
23
|
+
const { contract, response, responseBody } = await requestContract(definition.capabilityId, {
|
|
24
|
+
pathParams: inferPathParams(contractFor(definition.capabilityId), definition.pathParams)
|
|
25
|
+
});
|
|
26
|
+
assertCondition(response.status === contract.endpoint.successStatus, \`seed greeting readiness expected \${contract.endpoint.successStatus}, got \${response.status}\`);
|
|
27
|
+
assertCondition(responseBody?.id === envValue(plan.env.demoPrimaryId), "seed greeting readiness did not return the expected demo greeting");
|
|
28
|
+
} else if (definition.id === "create_greeting") {
|
|
29
|
+
const { contract, response, responseBody } = await requestContract(definition.capabilityId, {
|
|
30
|
+
payload: createPayload()
|
|
31
|
+
});
|
|
32
|
+
assertCondition(response.status === contract.endpoint.successStatus, \`create greeting expected \${contract.endpoint.successStatus}, got \${response.status}\`);
|
|
33
|
+
assertCondition(responseBody?.id, "create greeting response did not include id");
|
|
34
|
+
assertCondition(responseBody?.message === "Hello from the runtime check", "create greeting did not persist message");
|
|
35
|
+
state.createdPrimary = responseBody;
|
|
36
|
+
state.latestPrimary = responseBody;
|
|
37
|
+
result.resources.primaryId = responseBody.id;
|
|
38
|
+
} else if (definition.id === "get_created_greeting") {
|
|
39
|
+
const { contract, response, responseBody } = await requestContract(definition.capabilityId);
|
|
40
|
+
assertCondition(response.status === contract.endpoint.successStatus, \`get greeting expected \${contract.endpoint.successStatus}, got \${response.status}\`);
|
|
41
|
+
assertCondition(responseBody?.id === state.latestPrimary?.id, "get greeting did not return the created greeting");
|
|
42
|
+
assertCondition(responseBody?.message, "get greeting did not include message");
|
|
43
|
+
} else if (definition.id === "list_greetings") {
|
|
44
|
+
const { contract, response, responseBody } = await requestContract(definition.capabilityId);
|
|
45
|
+
assertCondition(response.status === contract.endpoint.successStatus, \`list greetings expected \${contract.endpoint.successStatus}, got \${response.status}\`);
|
|
46
|
+
assertCondition(Array.isArray(responseBody?.items), "list greetings did not return an items array");
|
|
47
|
+
assertCondition(responseBody.items.some((item) => item.id === state.latestPrimary?.id), "list greetings did not include the created greeting");
|
|
48
|
+
} else if (definition.id === "update_greeting") {
|
|
49
|
+
const { contract, response, responseBody } = await requestContract(definition.capabilityId, {
|
|
50
|
+
payload: {
|
|
51
|
+
message: "Hello from the updated runtime check"
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
assertCondition(response.status === contract.endpoint.successStatus, \`update greeting expected \${contract.endpoint.successStatus}, got \${response.status}\`);
|
|
55
|
+
assertCondition(responseBody?.message === "Hello from the updated runtime check", "update greeting did not persist message");
|
|
56
|
+
state.latestPrimary = responseBody;
|
|
57
|
+
result.resources.primaryId = responseBody.id;
|
|
58
|
+
} else if (definition.id === "invalid_create_returns_4xx") {
|
|
59
|
+
const { response, responseBody } = await requestContract(definition.capabilityId, {
|
|
60
|
+
payload: {}
|
|
61
|
+
});
|
|
62
|
+
assertCondition(Math.floor(response.status / 100) === definition.expectStatusClass, \`invalid create expected \${definition.expectStatusClass}xx, got \${response.status}\`);
|
|
63
|
+
assertErrorResponse(responseBody, definition.expectErrorCode, "invalid create");
|
|
64
|
+
} else if (definition.id === "get_unknown_greeting_not_found") {
|
|
65
|
+
const { response, responseBody } = await requestContract(definition.capabilityId, {
|
|
66
|
+
pathParams: {
|
|
67
|
+
id: crypto.randomUUID()
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
assertCondition(response.status === definition.expectStatus, \`get unknown greeting expected \${definition.expectStatus}, got \${response.status}\`);
|
|
71
|
+
assertErrorResponse(responseBody, definition.expectErrorCode, "get unknown greeting");`;
|
|
72
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const HELLO_RUNTIME_CHECKS = {
|
|
2
|
+
environmentStage: {
|
|
3
|
+
id: "environment",
|
|
4
|
+
name: "Environment Readiness",
|
|
5
|
+
failFast: true,
|
|
6
|
+
checks: [
|
|
7
|
+
{
|
|
8
|
+
id: "required_env",
|
|
9
|
+
kind: "env_required",
|
|
10
|
+
mandatory: true,
|
|
11
|
+
mutating: false
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "web_home_ready",
|
|
15
|
+
kind: "web_contract",
|
|
16
|
+
path: "/",
|
|
17
|
+
expectStatus: 200,
|
|
18
|
+
expectText: "Topogram Starter",
|
|
19
|
+
mandatory: true,
|
|
20
|
+
mutating: false
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "api_health_ready",
|
|
24
|
+
kind: "api_health",
|
|
25
|
+
path: "/health",
|
|
26
|
+
expectStatus: 200,
|
|
27
|
+
expectOk: true,
|
|
28
|
+
mandatory: true,
|
|
29
|
+
mutating: false
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "api_ready",
|
|
33
|
+
kind: "api_ready",
|
|
34
|
+
path: "/ready",
|
|
35
|
+
expectStatus: 200,
|
|
36
|
+
expectReady: true,
|
|
37
|
+
mandatory: true,
|
|
38
|
+
mutating: false
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "api_seed_greeting_ready",
|
|
42
|
+
kind: "api_contract",
|
|
43
|
+
capabilityId: "cap_get_greeting",
|
|
44
|
+
pathParams: {
|
|
45
|
+
greeting_id: "$env:TOPOGRAM_DEMO_PRIMARY_ID"
|
|
46
|
+
},
|
|
47
|
+
mandatory: true,
|
|
48
|
+
mutating: false
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
apiStage: {
|
|
53
|
+
id: "api",
|
|
54
|
+
name: "API Contract Checks",
|
|
55
|
+
checks: [
|
|
56
|
+
{ id: "create_greeting", kind: "api_contract", capabilityId: "cap_create_greeting", mutating: true, mandatory: true },
|
|
57
|
+
{ id: "get_created_greeting", kind: "api_contract", capabilityId: "cap_get_greeting", mutating: false, mandatory: true },
|
|
58
|
+
{ id: "list_greetings", kind: "api_contract", capabilityId: "cap_list_greetings", mutating: false, mandatory: true },
|
|
59
|
+
{ id: "update_greeting", kind: "api_contract", capabilityId: "cap_update_greeting", mutating: true, mandatory: true },
|
|
60
|
+
{ id: "invalid_create_returns_4xx", kind: "api_negative", capabilityId: "cap_create_greeting", expectStatusClass: 4, expectErrorCode: "cap_create_greeting_invalid_request", mutating: false, mandatory: true },
|
|
61
|
+
{ id: "get_unknown_greeting_not_found", kind: "api_negative", capabilityId: "cap_get_greeting", expectStatus: 404, expectErrorCode: "cap_get_greeting_not_found", mutating: false, mandatory: true }
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
smokeChecks: [
|
|
65
|
+
{ id: "web_home_page", type: "web_get", path: "/", expectStatus: 200, expectText: "Topogram Starter" },
|
|
66
|
+
{ id: "create_greeting", type: "api_post", path: "/greetings", expectStatus: 201, capabilityId: "cap_create_greeting" },
|
|
67
|
+
{ id: "get_greeting", type: "api_get", path: "/greetings/:id", expectStatus: 200, capabilityId: "cap_get_greeting" },
|
|
68
|
+
{ id: "list_greetings", type: "api_get", path: "/greetings", expectStatus: 200, capabilityId: "cap_list_greetings" }
|
|
69
|
+
]
|
|
70
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { HELLO_BACKEND_REFERENCE } from "../backend/reference.js";
|
|
2
|
+
|
|
3
|
+
export const HELLO_RUNTIME_REFERENCE = {
|
|
4
|
+
localDbProjectionId: "proj_db_postgres",
|
|
5
|
+
serviceName: HELLO_BACKEND_REFERENCE.serviceName,
|
|
6
|
+
ports: {
|
|
7
|
+
server: 3000,
|
|
8
|
+
web: 5173
|
|
9
|
+
},
|
|
10
|
+
environment: {
|
|
11
|
+
name: "Starter Local Runtime Stack",
|
|
12
|
+
databaseName: "topogram_starter",
|
|
13
|
+
envExample: `PUBLIC_TOPOGRAM_DEMO_PRIMARY_ID=${HELLO_BACKEND_REFERENCE.demo.greetingId}
|
|
14
|
+
TOPOGRAM_DEMO_PRIMARY_ID=${HELLO_BACKEND_REFERENCE.demo.greetingId}
|
|
15
|
+
TOPOGRAM_DEMO_MESSAGE=${HELLO_BACKEND_REFERENCE.demo.message}`
|
|
16
|
+
},
|
|
17
|
+
compileCheck: {
|
|
18
|
+
name: "Starter Generated Compile Checks"
|
|
19
|
+
},
|
|
20
|
+
smoke: {
|
|
21
|
+
name: "Starter Runtime Smoke Plan",
|
|
22
|
+
bundleTitle: "Starter Runtime Smoke Bundle",
|
|
23
|
+
defaultContainerEnvVar: "TOPOGRAM_DEMO_MESSAGE",
|
|
24
|
+
webPath: "/",
|
|
25
|
+
expectText: "Topogram Starter",
|
|
26
|
+
createPath: "/greetings",
|
|
27
|
+
getPathPrefix: "/greetings/",
|
|
28
|
+
listPath: "/greetings",
|
|
29
|
+
createPayload: {
|
|
30
|
+
title: "Smoke Test Greeting",
|
|
31
|
+
containerField: "message"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
runtimeCheck: {
|
|
35
|
+
name: "Starter Runtime Check Plan",
|
|
36
|
+
bundleTitle: "Starter Runtime Check Bundle",
|
|
37
|
+
requiredEnv: [
|
|
38
|
+
"TOPOGRAM_API_BASE_URL",
|
|
39
|
+
"TOPOGRAM_WEB_BASE_URL",
|
|
40
|
+
"TOPOGRAM_DEMO_PRIMARY_ID"
|
|
41
|
+
],
|
|
42
|
+
demoContainerEnvVar: "TOPOGRAM_DEMO_MESSAGE",
|
|
43
|
+
demoPrimaryEnvVar: "TOPOGRAM_DEMO_PRIMARY_ID",
|
|
44
|
+
lookupPaths: {},
|
|
45
|
+
stageNotes: [
|
|
46
|
+
{
|
|
47
|
+
id: "environment",
|
|
48
|
+
summary: "required env, web readiness, API health, API readiness, and seeded greeting lookup"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "api",
|
|
52
|
+
summary: "core greeting create, get, list, and update paths"
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
notes: [
|
|
56
|
+
"Mutating checks create and update a runtime-check greeting.",
|
|
57
|
+
"Later stages are skipped if environment readiness fails.",
|
|
58
|
+
"The generated server exposes both `/health` and `/ready`.",
|
|
59
|
+
"Use the smoke bundle for a faster minimal confidence check.",
|
|
60
|
+
"Use this runtime-check bundle for staged verification and JSON reporting."
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
appBundle: {
|
|
64
|
+
name: "Topogram Starter App Bundle",
|
|
65
|
+
demoContainerName: HELLO_BACKEND_REFERENCE.demo.message,
|
|
66
|
+
demoPrimaryTitle: HELLO_BACKEND_REFERENCE.demo.message
|
|
67
|
+
},
|
|
68
|
+
demoEnv: {
|
|
69
|
+
userId: "",
|
|
70
|
+
containerId: HELLO_BACKEND_REFERENCE.demo.message,
|
|
71
|
+
primaryId: HELLO_BACKEND_REFERENCE.demo.greetingId
|
|
72
|
+
}
|
|
73
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const HELLO_WEB_REFERENCE = {
|
|
2
|
+
brandName: "Topogram Starter",
|
|
3
|
+
client: {
|
|
4
|
+
primaryParam: "greeting_id",
|
|
5
|
+
functionNames: {
|
|
6
|
+
list: "listGreetings",
|
|
7
|
+
get: "getGreeting",
|
|
8
|
+
create: "createGreeting",
|
|
9
|
+
update: "updateGreeting",
|
|
10
|
+
terminal: "reloadGreeting"
|
|
11
|
+
},
|
|
12
|
+
capabilityIds: {
|
|
13
|
+
list: "cap_list_greetings",
|
|
14
|
+
get: "cap_get_greeting",
|
|
15
|
+
create: "cap_create_greeting",
|
|
16
|
+
update: "cap_update_greeting",
|
|
17
|
+
terminal: "cap_get_greeting"
|
|
18
|
+
},
|
|
19
|
+
extraFunctions: []
|
|
20
|
+
},
|
|
21
|
+
nav: {
|
|
22
|
+
browseLabel: "Starter",
|
|
23
|
+
browseRoute: "/",
|
|
24
|
+
createLabel: "API Contract",
|
|
25
|
+
createRoute: "/",
|
|
26
|
+
links: [
|
|
27
|
+
{ label: "Home", route: "/" }
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
home: {
|
|
31
|
+
demoPrimaryEnvVar: "PUBLIC_TOPOGRAM_DEMO_PRIMARY_ID",
|
|
32
|
+
demoTaskLabel: "Seeded Greeting ID",
|
|
33
|
+
heroDescriptionTemplate: "Generated from Topogram via the PROFILE profile and wired to a neutral API, web surface, and database.",
|
|
34
|
+
dynamicRouteText: "This screen uses a dynamic route.",
|
|
35
|
+
noRouteText: "No direct route is exposed for this screen."
|
|
36
|
+
},
|
|
37
|
+
createPrimary: {
|
|
38
|
+
defaultAssigneeEnvVar: "PUBLIC_TOPOGRAM_DEMO_USER_ID",
|
|
39
|
+
defaultContainerEnvVar: "PUBLIC_TOPOGRAM_DEMO_MESSAGE",
|
|
40
|
+
helperText: "Create a greeting through the generated API.",
|
|
41
|
+
projectPlaceholder: "Greeting message",
|
|
42
|
+
cancelLabel: "Cancel",
|
|
43
|
+
submitLabel: "Create Greeting"
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function renderHelloHomePage({
|
|
2
|
+
useTypescript,
|
|
3
|
+
demoPrimaryEnvVar,
|
|
4
|
+
screens,
|
|
5
|
+
projectionName,
|
|
6
|
+
homeDescription
|
|
7
|
+
}) {
|
|
8
|
+
return `<script${useTypescript ? ' lang="ts"' : ""}>
|
|
9
|
+
import { ${demoPrimaryEnvVar} as DEMO_GREETING_ID } from "$env/static/public";
|
|
10
|
+
|
|
11
|
+
const screens = ${JSON.stringify(screens, null, 2)};
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<main>
|
|
15
|
+
<div class="stack">
|
|
16
|
+
<section class="card hero">
|
|
17
|
+
<div>
|
|
18
|
+
<p class="muted">Generated starter</p>
|
|
19
|
+
<h1>Topogram Starter</h1>
|
|
20
|
+
<p>${homeDescription}</p>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="button-row">
|
|
23
|
+
<a class="button-link" href="/greetings">View API route</a>
|
|
24
|
+
{#if DEMO_GREETING_ID}
|
|
25
|
+
<span class="badge">Seed: {DEMO_GREETING_ID}</span>
|
|
26
|
+
{/if}
|
|
27
|
+
</div>
|
|
28
|
+
</section>
|
|
29
|
+
|
|
30
|
+
<section class="grid two">
|
|
31
|
+
{#each screens as screen}
|
|
32
|
+
<article class="card">
|
|
33
|
+
<h2>{screen.title}</h2>
|
|
34
|
+
<p class="muted">{screen.route || "Contract-only screen"}</p>
|
|
35
|
+
</article>
|
|
36
|
+
{/each}
|
|
37
|
+
</section>
|
|
38
|
+
</div>
|
|
39
|
+
</main>
|
|
40
|
+
`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function renderHelloRoutes() {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@topogram/starter-web-api-db",
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "SvelteKit, Hono, and Postgres Topogram starter.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"private": false,
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"topogram-template.json",
|
|
10
|
+
"topogram",
|
|
11
|
+
"topogram.project.json",
|
|
12
|
+
"implementation",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@topogram/generator-hono-api": "0.2.6",
|
|
17
|
+
"@topogram/generator-postgres-db": "0.1.6",
|
|
18
|
+
"@topogram/generator-sveltekit-web": "0.1.14"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"registry": "https://registry.npmjs.org",
|
|
22
|
+
"access": "public"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
capability cap_update_greeting {
|
|
2
|
+
name "Update Greeting"
|
|
3
|
+
description "Updates one greeting"
|
|
4
|
+
|
|
5
|
+
actors [actor_operator]
|
|
6
|
+
reads [entity_greeting]
|
|
7
|
+
updates [entity_greeting]
|
|
8
|
+
input [shape_input_update_greeting]
|
|
9
|
+
output [shape_output_greeting_detail]
|
|
10
|
+
|
|
11
|
+
status active
|
|
12
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
entity entity_greeting {
|
|
2
|
+
name "Greeting"
|
|
3
|
+
description "A minimal resource used by the neutral starter"
|
|
4
|
+
|
|
5
|
+
fields {
|
|
6
|
+
id uuid required
|
|
7
|
+
message text required
|
|
8
|
+
created_at datetime required
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
keys {
|
|
12
|
+
primary [id]
|
|
13
|
+
index [created_at]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
invariants {
|
|
17
|
+
message length <= 240
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
status active
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
projection proj_api {
|
|
2
|
+
name "Starter API"
|
|
3
|
+
description "HTTP API realization for the neutral starter"
|
|
4
|
+
platform dotnet
|
|
5
|
+
|
|
6
|
+
realizes [cap_create_greeting, cap_get_greeting, cap_list_greetings, cap_update_greeting]
|
|
7
|
+
outputs [database_schema, entity_models, request_contracts, response_contracts, endpoints]
|
|
8
|
+
|
|
9
|
+
http {
|
|
10
|
+
cap_create_greeting method POST path /greetings success 201 auth none request body
|
|
11
|
+
cap_get_greeting method GET path /greetings/:id success 200 auth none request path
|
|
12
|
+
cap_list_greetings method GET path /greetings success 200 auth none request query
|
|
13
|
+
cap_update_greeting method PATCH path /greetings/:id success 200 auth none request body
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
http_fields {
|
|
17
|
+
cap_get_greeting input greeting_id in path as id
|
|
18
|
+
cap_update_greeting input greeting_id in path as id
|
|
19
|
+
cap_update_greeting input message in body
|
|
20
|
+
cap_list_greetings input after in query
|
|
21
|
+
cap_list_greetings input limit in query
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
http_responses {
|
|
25
|
+
cap_list_greetings mode cursor item shape_output_greeting_detail cursor request_after after response_next next_cursor limit field limit default 25 max 100 sort by created_at direction desc total included false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
status active
|
|
29
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
projection proj_db_postgres {
|
|
2
|
+
name "Starter Postgres DB"
|
|
3
|
+
description "Postgres persistence realization for the neutral starter"
|
|
4
|
+
platform db_postgres
|
|
5
|
+
|
|
6
|
+
realizes [entity_greeting]
|
|
7
|
+
outputs [db_contract, sql_schema]
|
|
8
|
+
|
|
9
|
+
db_tables {
|
|
10
|
+
entity_greeting table greetings
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
db_keys {
|
|
14
|
+
entity_greeting primary [id]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
db_indexes {
|
|
18
|
+
entity_greeting index [created_at]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
db_lifecycle {
|
|
22
|
+
entity_greeting timestamps created_at created_at updated_at created_at
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
generator_defaults {
|
|
26
|
+
profile postgres_sql
|
|
27
|
+
language sql
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
status active
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
projection proj_ui_shared {
|
|
2
|
+
name "Starter Shared UI"
|
|
3
|
+
description "Platform-neutral UI semantics for the neutral starter"
|
|
4
|
+
platform ui_shared
|
|
5
|
+
|
|
6
|
+
realizes [cap_list_greetings, cap_get_greeting, cap_create_greeting, cap_update_greeting]
|
|
7
|
+
outputs [ui_contract]
|
|
8
|
+
|
|
9
|
+
ui_app_shell {
|
|
10
|
+
brand "Topogram Starter"
|
|
11
|
+
shell topbar
|
|
12
|
+
global_search false
|
|
13
|
+
notifications false
|
|
14
|
+
account_menu false
|
|
15
|
+
footer none
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
ui_screens {
|
|
19
|
+
screen greeting_list kind list title "Greetings" load cap_list_greetings item_shape shape_output_greeting_card detail_capability cap_get_greeting primary_action cap_create_greeting empty_title "No greetings yet" empty_body "Create a greeting to verify the generated stack" loading_state skeleton error_state inline
|
|
20
|
+
screen greeting_detail kind detail title "Greeting Details" load cap_get_greeting view_shape shape_output_greeting_detail primary_action cap_update_greeting loading_state skeleton
|
|
21
|
+
screen greeting_create kind form title "Create Greeting" input_shape shape_input_create_greeting submit cap_create_greeting success_navigate greeting_detail success_state banner
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
ui_navigation {
|
|
25
|
+
group starter label "Starter" placement primary pattern primary
|
|
26
|
+
screen greeting_list group starter label "Greetings" order 10 visible true default true sitemap include pattern primary
|
|
27
|
+
screen greeting_create group starter label "Create" order 20 visible true sitemap include pattern primary
|
|
28
|
+
screen greeting_detail group starter label "Greeting Details" visible false breadcrumb greeting_list sitemap exclude pattern stack_navigation
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
ui_screen_regions {
|
|
32
|
+
screen greeting_list region hero pattern summary_stats placement primary
|
|
33
|
+
screen greeting_list region results pattern resource_table placement primary
|
|
34
|
+
screen greeting_detail region summary pattern detail_panel placement primary
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
status active
|
|
38
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
projection proj_ui_web {
|
|
2
|
+
name "Starter Web UI"
|
|
3
|
+
description "Web realization for the neutral starter using a SvelteKit profile"
|
|
4
|
+
platform ui_web
|
|
5
|
+
|
|
6
|
+
realizes [proj_ui_shared, cap_list_greetings, cap_get_greeting, cap_create_greeting, cap_update_greeting]
|
|
7
|
+
outputs [ui_contract, web_app]
|
|
8
|
+
|
|
9
|
+
ui_routes {
|
|
10
|
+
screen greeting_list path /greetings
|
|
11
|
+
screen greeting_detail path /greetings/:id
|
|
12
|
+
screen greeting_create path /greetings/new
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
ui_web {
|
|
16
|
+
screen greeting_list shell topbar
|
|
17
|
+
screen greeting_list layout responsive_collection
|
|
18
|
+
screen greeting_list desktop_variant table
|
|
19
|
+
screen greeting_list mobile_variant cards
|
|
20
|
+
screen greeting_list collection table
|
|
21
|
+
screen greeting_detail layout detail_page
|
|
22
|
+
screen greeting_create present page
|
|
23
|
+
action cap_create_greeting present button
|
|
24
|
+
action cap_create_greeting placement toolbar
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
generator_defaults {
|
|
28
|
+
profile sveltekit
|
|
29
|
+
language typescript
|
|
30
|
+
styling css
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
status active
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
verification ver_runtime_flow {
|
|
2
|
+
name "Starter runtime flow"
|
|
3
|
+
description "Verifies the core greeting runtime behavior."
|
|
4
|
+
|
|
5
|
+
validates [cap_create_greeting, cap_get_greeting, cap_list_greetings, cap_update_greeting]
|
|
6
|
+
method runtime
|
|
7
|
+
|
|
8
|
+
scenarios [
|
|
9
|
+
create_greeting_runtime,
|
|
10
|
+
get_created_greeting_runtime,
|
|
11
|
+
list_greetings_runtime,
|
|
12
|
+
update_greeting_runtime
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
status active
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
verification ver_runtime_smoke {
|
|
2
|
+
name "Starter runtime smoke"
|
|
3
|
+
description "Covers the minimum web and API checks for the neutral starter."
|
|
4
|
+
|
|
5
|
+
validates [cap_create_greeting, cap_get_greeting, cap_list_greetings]
|
|
6
|
+
method smoke
|
|
7
|
+
|
|
8
|
+
scenarios [
|
|
9
|
+
home_page_responds,
|
|
10
|
+
create_greeting_smoke,
|
|
11
|
+
get_created_greeting_smoke,
|
|
12
|
+
list_greetings_smoke
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
status active
|
|
16
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.1",
|
|
3
|
+
"implementation": {
|
|
4
|
+
"id": "web-api-db-template",
|
|
5
|
+
"module": "./implementation/index.js",
|
|
6
|
+
"export": "WEB_API_DB_IMPLEMENTATION"
|
|
7
|
+
},
|
|
8
|
+
"outputs": {
|
|
9
|
+
"app": {
|
|
10
|
+
"path": "./app",
|
|
11
|
+
"ownership": "generated"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"topology": {
|
|
15
|
+
"components": [
|
|
16
|
+
{
|
|
17
|
+
"id": "app_api",
|
|
18
|
+
"type": "api",
|
|
19
|
+
"projection": "proj_api",
|
|
20
|
+
"generator": {
|
|
21
|
+
"id": "@topogram/generator-hono-api",
|
|
22
|
+
"version": "1",
|
|
23
|
+
"package": "@topogram/generator-hono-api"
|
|
24
|
+
},
|
|
25
|
+
"port": 3000,
|
|
26
|
+
"database": "app_postgres"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "app_sveltekit",
|
|
30
|
+
"type": "web",
|
|
31
|
+
"projection": "proj_ui_web",
|
|
32
|
+
"generator": {
|
|
33
|
+
"id": "@topogram/generator-sveltekit-web",
|
|
34
|
+
"version": "1",
|
|
35
|
+
"package": "@topogram/generator-sveltekit-web"
|
|
36
|
+
},
|
|
37
|
+
"port": 5173,
|
|
38
|
+
"api": "app_api"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "app_postgres",
|
|
42
|
+
"type": "database",
|
|
43
|
+
"projection": "proj_db_postgres",
|
|
44
|
+
"generator": {
|
|
45
|
+
"id": "@topogram/generator-postgres-db",
|
|
46
|
+
"version": "1",
|
|
47
|
+
"package": "@topogram/generator-postgres-db"
|
|
48
|
+
},
|
|
49
|
+
"port": 5432
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|