@rawsql-ts/ztd-cli 0.16.0 → 0.17.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 +33 -20
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.js +372 -127
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/lint.d.ts +4 -4
- package/dist/commands/lint.js +60 -40
- package/dist/commands/lint.js.map +1 -1
- package/dist/commands/ztdConfig.d.ts +2 -2
- package/dist/commands/ztdConfig.js +26 -12
- package/dist/commands/ztdConfig.js.map +1 -1
- package/dist/utils/optionalDependencies.d.ts +35 -0
- package/dist/utils/optionalDependencies.js +96 -0
- package/dist/utils/optionalDependencies.js.map +1 -0
- package/package.json +16 -9
- package/templates/AGENTS.md +36 -309
- package/templates/README.md +12 -215
- package/templates/dist/ztd-cli/templates/src/db/sql-client.ts +24 -0
- package/templates/src/AGENTS.md +26 -0
- package/templates/src/catalog/AGENTS.md +37 -0
- package/templates/src/catalog/runtime/AGENTS.md +75 -0
- package/templates/src/catalog/runtime/_coercions.ts +1 -0
- package/templates/src/catalog/runtime/_smoke.runtime.ts +21 -0
- package/templates/src/catalog/specs/AGENTS.md +48 -0
- package/templates/src/catalog/specs/_smoke.spec.arktype.ts +21 -0
- package/templates/src/catalog/specs/_smoke.spec.zod.ts +20 -0
- package/templates/src/db/sql-client.ts +5 -5
- package/templates/src/jobs/AGENTS.md +26 -0
- package/templates/src/jobs/README.md +3 -0
- package/templates/src/repositories/AGENTS.md +118 -0
- package/templates/src/repositories/tables/AGENTS.md +94 -0
- package/templates/src/repositories/tables/README.md +3 -0
- package/templates/src/repositories/views/AGENTS.md +25 -0
- package/templates/src/repositories/views/README.md +3 -0
- package/templates/src/sql/AGENTS.md +77 -0
- package/templates/src/sql/README.md +6 -0
- package/templates/tests/AGENTS.md +43 -150
- package/templates/tests/generated/AGENTS.md +16 -0
- package/templates/tests/smoke.test.ts +5 -0
- package/templates/tests/smoke.validation.test.ts +34 -0
- package/templates/tests/support/AGENTS.md +26 -0
- package/templates/tests/support/global-setup.ts +8 -23
- package/templates/tests/support/testkit-client.ts +13 -741
- package/templates/tsconfig.json +8 -1
- package/templates/ztd/AGENTS.md +11 -67
- package/templates/ztd/README.md +4 -13
- package/templates/ztd/ddl/AGENTS.md +34 -0
- package/templates/ztd/ddl/demo.sql +74 -0
- package/templates/src/repositories/user-accounts.ts +0 -179
- package/templates/tests/user-profiles.test.ts +0 -161
- package/templates/tests/writer-constraints.test.ts +0 -32
package/templates/tsconfig.json
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"extends": "../../../tsconfig.json",
|
|
3
2
|
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022"],
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"moduleResolution": "NodeNext",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
4
11
|
"outDir": "dist",
|
|
5
12
|
"tsBuildInfoFile": "dist/.tsbuildinfo"
|
|
6
13
|
},
|
package/templates/ztd/AGENTS.md
CHANGED
|
@@ -1,74 +1,18 @@
|
|
|
1
|
-
# AGENTS
|
|
1
|
+
# ztd AGENTS
|
|
2
2
|
|
|
3
|
-
This
|
|
4
|
-
AI must treat these directories as authoritative sources of truth and must not modify them without explicit instruction.
|
|
5
|
-
These rules govern the `ztd/` contents after project initialization and apply regardless of mapper, writer, or runtime architecture decisions.
|
|
3
|
+
This directory contains ZTD inputs and related documentation.
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
## Core rule
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
- "ztd/ddl/" is the only human-owned source of truth inside "ztd/".
|
|
8
|
+
- Do not create new subdirectories under "ztd/" unless explicitly instructed.
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
- After cloning the repository (or in a clean environment), run `npx ztd ztd-config`.
|
|
13
|
-
- If TypeScript reports missing modules or type errors because `tests/generated/` is missing, run `npx ztd ztd-config`.
|
|
14
|
-
- Generated artifacts exist solely to support validation and testing and MUST NEVER influence definitions under `ztd/`.
|
|
10
|
+
## Boundaries
|
|
15
11
|
|
|
16
|
-
|
|
12
|
+
- Runtime code must not depend on "ztd/".
|
|
13
|
+
- Tests may reference DDL and generated outputs via ZTD tooling.
|
|
17
14
|
|
|
18
|
-
##
|
|
15
|
+
## Editing policy
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
### Purpose
|
|
23
|
-
|
|
24
|
-
This directory contains all canonical definitions of database structure, including:
|
|
25
|
-
|
|
26
|
-
- CREATE TABLE
|
|
27
|
-
- ALTER TABLE
|
|
28
|
-
- Constraints
|
|
29
|
-
- Indexes
|
|
30
|
-
|
|
31
|
-
It is the **single source of truth** for the physical database schema as interpreted by ztd-cli.
|
|
32
|
-
|
|
33
|
-
### Behavior Rules (strict)
|
|
34
|
-
|
|
35
|
-
- **Never modify files in this directory unless explicitly instructed by a human.**
|
|
36
|
-
- Do not apply “helpful” refactors, cleanups, or formatting changes on your own.
|
|
37
|
-
- You may propose edits or review changes when asked, but you must not apply them without approval.
|
|
38
|
-
- All DDL statements must be:
|
|
39
|
-
- Valid PostgreSQL syntax
|
|
40
|
-
- Explicitly semicolon-terminated
|
|
41
|
-
- Do not reorder statements; dependency and execution order matters.
|
|
42
|
-
- Preserve all human-authored:
|
|
43
|
-
- Naming
|
|
44
|
-
- Formatting
|
|
45
|
-
- Comments
|
|
46
|
-
- Structural intent
|
|
47
|
-
- When asked to extend existing definitions:
|
|
48
|
-
- Do not remove or rewrite existing columns or comments unless explicitly told.
|
|
49
|
-
- Maintain column order and constraint style.
|
|
50
|
-
- Do not introduce schema changes that conflict with existing constraints or indexes.
|
|
51
|
-
- The `public.user_account` and `public.user_profile` tables exist to support the mapper/writer sample; any modification to those tables is a maintenance obligation that requires concurrent updates to `src/repositories/user-accounts.ts` and `tests/writer-constraints.test.ts` so the workflow keeps functioning.
|
|
52
|
-
- DDL defines physical truth only and MUST NEVER be reshaped to accommodate mapper, writer, or test tooling.
|
|
53
|
-
- Runtime convenience is never a valid reason to alter DDL.
|
|
54
|
-
|
|
55
|
-
If there is uncertainty, stop and request clarification instead of guessing.
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
- Only `ztd/ddl` is part of the canonical `ztd` contract; do not create or assume additional subdirectories without explicit human direction.
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## Absolute Restrictions (important)
|
|
64
|
-
|
|
65
|
-
- AI must not modify anything under `ztd/` by default.
|
|
66
|
-
- DDL is the only **human-led artifact** in this directory.
|
|
67
|
-
- AI may assist by:
|
|
68
|
-
- Reading
|
|
69
|
-
- Explaining
|
|
70
|
-
- Proposing diffs
|
|
71
|
-
- AI may apply changes **only** with explicit instruction.
|
|
72
|
-
- No tooling limitation, test strategy, or runtime design justifies modifying `ztd/` artifacts.
|
|
73
|
-
|
|
74
|
-
Violation of these rules leads to silent corruption of domain meaning and is unacceptable.
|
|
17
|
+
- Avoid modifying "ztd/README.md" unless explicitly asked.
|
|
18
|
+
- Prefer adding rules to "ztd/ddl/AGENTS.md" for DDL-related guidance.
|
package/templates/ztd/README.md
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ztd/
|
|
2
2
|
|
|
3
|
-
This directory
|
|
3
|
+
This directory stores ZTD schema artifacts.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- Place every schema definition under `ztd/ddl/` with valid PostgreSQL and semicolon-terminated statements.
|
|
8
|
-
- Keep the files deterministic: avoid generated output, enforce column ordering, and document any non-obvious constraints with comments.
|
|
9
|
-
- When you rename or drop a column, update the corresponding DDL file rather than trying to patch test artifacts manually.
|
|
10
|
-
- Treat `ztd/ddl/` as a human-maintained catalog; AI may assist but must not invent or diverge from the files stored there.
|
|
11
|
-
|
|
12
|
-
## Workflow expectations
|
|
13
|
-
|
|
14
|
-
- Regenerate `tests/generated/ztd-row-map.generated.ts` via `npx ztd ztd-config` whenever the DDL changes.
|
|
15
|
-
- Do not assume any other subdirectories under `/ztd` exist unless a human has explicitly created them for a specific purpose.
|
|
5
|
+
- `ztd/ddl/<schema>.sql` is the source of truth for schema.
|
|
6
|
+
- Run `npx ztd ztd-config` to regenerate `tests/generated` outputs.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# ztd/ddl AGENTS
|
|
2
|
+
|
|
3
|
+
This folder contains physical schema definitions (DDL). Human-led.
|
|
4
|
+
|
|
5
|
+
## Ownership
|
|
6
|
+
|
|
7
|
+
- Humans own DDL semantics.
|
|
8
|
+
- AI may assist with mechanical edits, but must not invent domain rules.
|
|
9
|
+
|
|
10
|
+
## Conventions (Postgres-oriented)
|
|
11
|
+
|
|
12
|
+
- Table names: singular (example: "user", "order", "invoice")
|
|
13
|
+
- Primary key: "serial8" (bigserial) by default
|
|
14
|
+
- Timestamps: default "current_timestamp" where applicable
|
|
15
|
+
|
|
16
|
+
## Comments (required)
|
|
17
|
+
|
|
18
|
+
Add comments for tables and important columns.
|
|
19
|
+
|
|
20
|
+
Postgres syntax:
|
|
21
|
+
- "comment on table <schema>.<table> is '...';"
|
|
22
|
+
- "comment on column <schema>.<table>.<column> is '...';"
|
|
23
|
+
|
|
24
|
+
## Constraints
|
|
25
|
+
|
|
26
|
+
- Prefer explicit NOT NULL where appropriate.
|
|
27
|
+
- Prefer explicit unique constraints for business keys.
|
|
28
|
+
- Foreign keys are allowed, but keep them intentional and explain rationale in comments.
|
|
29
|
+
|
|
30
|
+
## File strategy
|
|
31
|
+
|
|
32
|
+
- Keep schema DDL in a clear and reviewable form.
|
|
33
|
+
- If using one file per schema (example: "public.sql"), keep it consistent.
|
|
34
|
+
- Avoid random splitting unless there is a strong reason.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
create table "user" (
|
|
2
|
+
user_id serial8 primary key,
|
|
3
|
+
user_name text not null,
|
|
4
|
+
email text not null,
|
|
5
|
+
created_at timestamptz not null default current_timestamp
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
comment on table "user" is
|
|
9
|
+
'User master. Referenced by task_assignment.';
|
|
10
|
+
|
|
11
|
+
comment on column "user".user_id is
|
|
12
|
+
'Primary key of user.';
|
|
13
|
+
comment on column "user".user_name is
|
|
14
|
+
'Display name of the user.';
|
|
15
|
+
comment on column "user".email is
|
|
16
|
+
'Email address of the user.';
|
|
17
|
+
comment on column "user".created_at is
|
|
18
|
+
'Timestamp when the user was created.';
|
|
19
|
+
|
|
20
|
+
create table task (
|
|
21
|
+
task_id serial8 primary key,
|
|
22
|
+
title text not null,
|
|
23
|
+
status text not null,
|
|
24
|
+
priority integer not null,
|
|
25
|
+
due_date date,
|
|
26
|
+
created_at timestamptz not null default current_timestamp
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
comment on table task is
|
|
30
|
+
'Task entity. Current assignee is derived from task_assignment history.';
|
|
31
|
+
|
|
32
|
+
comment on column task.task_id is
|
|
33
|
+
'Primary key of task.';
|
|
34
|
+
comment on column task.title is
|
|
35
|
+
'Short description of the task.';
|
|
36
|
+
comment on column task.status is
|
|
37
|
+
'Task status. Example: open, in_progress, done.';
|
|
38
|
+
comment on column task.priority is
|
|
39
|
+
'Priority of the task. Higher value means higher priority.';
|
|
40
|
+
comment on column task.due_date is
|
|
41
|
+
'Optional due date of the task.';
|
|
42
|
+
comment on column task.created_at is
|
|
43
|
+
'Timestamp when the task was created.';
|
|
44
|
+
|
|
45
|
+
create table task_assignment (
|
|
46
|
+
task_assignment_id serial8 primary key,
|
|
47
|
+
task_id bigint not null,
|
|
48
|
+
user_id bigint not null,
|
|
49
|
+
assigned_at timestamptz not null default current_timestamp
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
comment on table task_assignment is
|
|
53
|
+
'Assignment history of tasks. Latest assigned_at defines current assignee.';
|
|
54
|
+
|
|
55
|
+
comment on column task_assignment.task_assignment_id is
|
|
56
|
+
'Primary key of task_assignment.';
|
|
57
|
+
comment on column task_assignment.task_id is
|
|
58
|
+
'Referenced task identifier.';
|
|
59
|
+
comment on column task_assignment.user_id is
|
|
60
|
+
'Referenced user identifier.';
|
|
61
|
+
comment on column task_assignment.assigned_at is
|
|
62
|
+
'Timestamp when the task was assigned to the user.';
|
|
63
|
+
|
|
64
|
+
create index idx_task_assignment_task
|
|
65
|
+
on task_assignment(task_id);
|
|
66
|
+
|
|
67
|
+
create index idx_task_assignment_task_time
|
|
68
|
+
on task_assignment(task_id, assigned_at desc);
|
|
69
|
+
|
|
70
|
+
comment on index idx_task_assignment_task is
|
|
71
|
+
'Index for joining task_assignment by task_id.';
|
|
72
|
+
|
|
73
|
+
comment on index idx_task_assignment_task_time is
|
|
74
|
+
'Index for resolving latest assignment per task.';
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import type { SqlClient } from '../db/sql-client';
|
|
2
|
-
import { createMapper, entity, toRowsExecutor } from '@rawsql-ts/mapper-core';
|
|
3
|
-
import { insert, Key, remove, update } from '@rawsql-ts/writer-core';
|
|
4
|
-
|
|
5
|
-
const userAccountTable = 'public.user_account';
|
|
6
|
-
|
|
7
|
-
type UserProfileRow = {
|
|
8
|
-
profileId: number;
|
|
9
|
-
userAccountId: number;
|
|
10
|
-
bio: string | null;
|
|
11
|
-
website: string | null;
|
|
12
|
-
verified: boolean;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* DTO that represents a user account with its optional profile information.
|
|
17
|
-
* @property {number} userAccountId The primary key for the user account.
|
|
18
|
-
* @property {string} username The canonical username.
|
|
19
|
-
* @property {string} email The account email address.
|
|
20
|
-
* @property {string} displayName The account display name.
|
|
21
|
-
* @property {Date} createdAt When the account was created.
|
|
22
|
-
* @property {Date} updatedAt When the account was last updated.
|
|
23
|
-
* @property {UserProfileRow} [profile] Optional profile payload joined from user_profile.
|
|
24
|
-
*/
|
|
25
|
-
export type UserAccountWithProfile = {
|
|
26
|
-
userAccountId: number;
|
|
27
|
-
username: string;
|
|
28
|
-
email: string;
|
|
29
|
-
displayName: string;
|
|
30
|
-
createdAt: Date;
|
|
31
|
-
updatedAt: Date;
|
|
32
|
-
profile?: UserProfileRow;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// Map the joined profile columns so we can hydrate nested objects later.
|
|
36
|
-
const profileMapping = entity<UserProfileRow>({
|
|
37
|
-
name: 'userProfile',
|
|
38
|
-
key: 'profileId',
|
|
39
|
-
columnMap: {
|
|
40
|
-
profileId: 'profile_id',
|
|
41
|
-
userAccountId: 'profile_user_account_id',
|
|
42
|
-
bio: 'bio',
|
|
43
|
-
website: 'website',
|
|
44
|
-
verified: 'verified',
|
|
45
|
-
},
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
const userAccountMapping = entity<UserAccountWithProfile>({
|
|
49
|
-
name: 'userAccount',
|
|
50
|
-
key: 'userAccountId',
|
|
51
|
-
columnMap: {
|
|
52
|
-
userAccountId: 'user_account_id',
|
|
53
|
-
username: 'username',
|
|
54
|
-
email: 'email',
|
|
55
|
-
displayName: 'display_name',
|
|
56
|
-
createdAt: 'created_at',
|
|
57
|
-
updatedAt: 'updated_at',
|
|
58
|
-
},
|
|
59
|
-
}).belongsTo('profile', profileMapping, 'userAccountId', { optional: true });
|
|
60
|
-
|
|
61
|
-
const userProfilesSql = `
|
|
62
|
-
SELECT
|
|
63
|
-
u.user_account_id,
|
|
64
|
-
u.username,
|
|
65
|
-
u.email,
|
|
66
|
-
u.display_name,
|
|
67
|
-
u.created_at,
|
|
68
|
-
u.updated_at,
|
|
69
|
-
p.profile_id,
|
|
70
|
-
p.user_account_id AS profile_user_account_id,
|
|
71
|
-
p.bio,
|
|
72
|
-
p.website,
|
|
73
|
-
p.verified
|
|
74
|
-
FROM public.user_account u
|
|
75
|
-
LEFT JOIN public.user_profile p ON p.user_account_id = u.user_account_id
|
|
76
|
-
ORDER BY u.user_account_id, p.profile_id;
|
|
77
|
-
`;
|
|
78
|
-
|
|
79
|
-
// Build a mapper that can translate snake_case columns into camelCase DTOs.
|
|
80
|
-
const createMapperForClient = (client: SqlClient) =>
|
|
81
|
-
createMapper(
|
|
82
|
-
toRowsExecutor((sql, params: unknown[] = []) =>
|
|
83
|
-
client.query<Record<string, unknown>>(sql, params),
|
|
84
|
-
),
|
|
85
|
-
{
|
|
86
|
-
// The explicit column maps enumerate the nested entity columns while keyTransform handles generic snake_to_camel conversions.
|
|
87
|
-
keyTransform: 'snake_to_camel',
|
|
88
|
-
coerceDates: true,
|
|
89
|
-
},
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Queries all user accounts together with their associated profiles.
|
|
94
|
-
* @param {SqlClient} client Client proxy that executes the mapper SQL.
|
|
95
|
-
* @returns {Promise<UserAccountWithProfile[]>} The joined account-with-profile rows.
|
|
96
|
-
*/
|
|
97
|
-
export async function listUserProfiles(
|
|
98
|
-
client: SqlClient,
|
|
99
|
-
): Promise<UserAccountWithProfile[]> {
|
|
100
|
-
const mapper = createMapperForClient(client);
|
|
101
|
-
return mapper.query(userProfilesSql, [], userAccountMapping);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Parameters required to insert a new user account.
|
|
106
|
-
* @property {string} username The requested username.
|
|
107
|
-
* @property {string} email The requested email address.
|
|
108
|
-
* @property {string} displayName The requested display name.
|
|
109
|
-
*/
|
|
110
|
-
export type NewUserAccount = {
|
|
111
|
-
username: string;
|
|
112
|
-
email: string;
|
|
113
|
-
displayName: string;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Payload describing the display name change for an existing account.
|
|
118
|
-
* @property {string} displayName The new display name to persist.
|
|
119
|
-
*/
|
|
120
|
-
export type DisplayNameUpdatePayload = {
|
|
121
|
-
displayName: string;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Builds an insert statement for the user_account writer.
|
|
126
|
-
* @param {NewUserAccount} input The normalized fields for the new account.
|
|
127
|
-
* @returns {ReturnType<typeof insert>} A well-formed insert statement for the user_account writer.
|
|
128
|
-
*/
|
|
129
|
-
export function buildInsertUserAccount(
|
|
130
|
-
input: NewUserAccount,
|
|
131
|
-
): ReturnType<typeof insert> {
|
|
132
|
-
return insert(userAccountTable, {
|
|
133
|
-
username: input.username,
|
|
134
|
-
email: input.email,
|
|
135
|
-
display_name: input.displayName,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Builds an update statement that refreshes the display name and timestamp.
|
|
141
|
-
* @param {Key} key The unique key identifying the row to update.
|
|
142
|
-
* @param {DisplayNameUpdatePayload} payload The new display name payload.
|
|
143
|
-
* @returns {ReturnType<typeof update>} A writer update statement that refreshes the display name and updated_at timestamp.
|
|
144
|
-
*/
|
|
145
|
-
export function buildUpdateDisplayName(
|
|
146
|
-
key: Key,
|
|
147
|
-
payload: DisplayNameUpdatePayload,
|
|
148
|
-
): ReturnType<typeof update> {
|
|
149
|
-
return update(
|
|
150
|
-
userAccountTable,
|
|
151
|
-
{
|
|
152
|
-
// Persist the new display name and bump the timestamp along with it.
|
|
153
|
-
display_name: payload.displayName,
|
|
154
|
-
updated_at: new Date(),
|
|
155
|
-
},
|
|
156
|
-
key,
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Builds a delete statement for the specified user account key.
|
|
162
|
-
* @param {Key} key Identifies the row to remove.
|
|
163
|
-
* @returns {ReturnType<typeof remove>} A writer delete statement for the matching user account.
|
|
164
|
-
*/
|
|
165
|
-
export function buildRemoveUserAccount(key: Key): ReturnType<typeof remove> {
|
|
166
|
-
return remove(userAccountTable, key);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Column sets that writer tests use to ensure only approved columns are touched.
|
|
171
|
-
* @property {readonly string[]} insertColumns Columns allowed for new account inserts.
|
|
172
|
-
* @property {readonly string[]} updateColumns Columns permitted during updates.
|
|
173
|
-
* @property {readonly string[]} immutableColumns Columns that must remain unchanged.
|
|
174
|
-
*/
|
|
175
|
-
export const userAccountWriterColumnSets = {
|
|
176
|
-
insertColumns: ['username', 'email', 'display_name'] as const,
|
|
177
|
-
updateColumns: ['display_name', 'updated_at'] as const,
|
|
178
|
-
immutableColumns: ['user_account_id', 'created_at'] as const,
|
|
179
|
-
};
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test, afterAll } from 'vitest';
|
|
2
|
-
import type { TableFixture, TestkitProvider } from '@rawsql-ts/testkit-core';
|
|
3
|
-
import { createTestkitProvider } from '@rawsql-ts/testkit-core';
|
|
4
|
-
import { createPgTestkitClient } from '@rawsql-ts/pg-testkit';
|
|
5
|
-
import { Pool } from 'pg';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import {
|
|
8
|
-
tableFixture,
|
|
9
|
-
tableSchemas,
|
|
10
|
-
TestRowMap,
|
|
11
|
-
} from './generated/ztd-row-map.generated';
|
|
12
|
-
import { listUserProfiles } from '../src/repositories/user-accounts';
|
|
13
|
-
|
|
14
|
-
const ddlDirectories = [path.resolve(__dirname, '../ztd/ddl')];
|
|
15
|
-
const skipReason = 'DATABASE_URL is not configured';
|
|
16
|
-
const configuredDatabaseUrl = process.env.DATABASE_URL?.trim();
|
|
17
|
-
const suiteTitle = configuredDatabaseUrl
|
|
18
|
-
? 'user profile mapper'
|
|
19
|
-
: `user profile mapper (skipped: ${skipReason})`;
|
|
20
|
-
const describeUserProfile = configuredDatabaseUrl
|
|
21
|
-
? describe
|
|
22
|
-
: (describe.skip as typeof describe);
|
|
23
|
-
let pool: Pool | undefined;
|
|
24
|
-
let providerPromise: Promise<TestkitProvider> | undefined;
|
|
25
|
-
|
|
26
|
-
// Lazily initialize the test provider so missing DATABASE_URL values do not trigger side effects.
|
|
27
|
-
function getProvider(): Promise<TestkitProvider> {
|
|
28
|
-
if (providerPromise) {
|
|
29
|
-
return providerPromise;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const databaseUrl = process.env.DATABASE_URL?.trim();
|
|
33
|
-
if (!databaseUrl) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
'Cannot initialize the repository testkit provider without DATABASE_URL.',
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
pool = new Pool({ connectionString: databaseUrl });
|
|
40
|
-
const activePool = pool;
|
|
41
|
-
providerPromise = createTestkitProvider({
|
|
42
|
-
connectionFactory: () => activePool.connect(),
|
|
43
|
-
resourceFactory: async (connection, fixtures) =>
|
|
44
|
-
createPgTestkitClient({
|
|
45
|
-
connectionFactory: () => connection,
|
|
46
|
-
tableRows: fixtures,
|
|
47
|
-
ddl: { directories: ddlDirectories },
|
|
48
|
-
}),
|
|
49
|
-
releaseResource: async (client) => {
|
|
50
|
-
await client.close();
|
|
51
|
-
},
|
|
52
|
-
disposeConnection: async (connection) => {
|
|
53
|
-
if (typeof connection.release === 'function') {
|
|
54
|
-
connection.release();
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (typeof connection.end === 'function') {
|
|
58
|
-
await connection.end();
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
return providerPromise;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
afterAll(async () => {
|
|
67
|
-
// Close resources only when initialization actually happened.
|
|
68
|
-
if (!providerPromise) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const provider = await providerPromise;
|
|
73
|
-
await provider.close();
|
|
74
|
-
if (pool) {
|
|
75
|
-
await pool.end();
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
function buildUserAccounts(): TestRowMap['public.user_account'][] {
|
|
80
|
-
return [
|
|
81
|
-
{
|
|
82
|
-
user_account_id: 1,
|
|
83
|
-
username: 'alpha',
|
|
84
|
-
email: 'alpha@example.com',
|
|
85
|
-
display_name: 'Alpha Tester',
|
|
86
|
-
created_at: '2025-12-01T08:00:00Z',
|
|
87
|
-
updated_at: '2025-12-01T09:00:00Z',
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
user_account_id: 2,
|
|
91
|
-
username: 'bravo',
|
|
92
|
-
email: 'bravo@example.com',
|
|
93
|
-
display_name: 'Bravo Builder',
|
|
94
|
-
created_at: '2025-12-02T10:00:00Z',
|
|
95
|
-
updated_at: '2025-12-02T11:00:00Z',
|
|
96
|
-
},
|
|
97
|
-
];
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function buildUserProfiles(): TestRowMap['public.user_profile'][] {
|
|
101
|
-
return [
|
|
102
|
-
{
|
|
103
|
-
profile_id: 101,
|
|
104
|
-
user_account_id: 1,
|
|
105
|
-
bio: 'Lead engineer and mapper advocate.',
|
|
106
|
-
website: 'https://example.com',
|
|
107
|
-
verified: true,
|
|
108
|
-
},
|
|
109
|
-
];
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function buildFixtures(): TableFixture[] {
|
|
113
|
-
return [
|
|
114
|
-
tableFixture(
|
|
115
|
-
'public.user_account',
|
|
116
|
-
buildUserAccounts(),
|
|
117
|
-
tableSchemas['public.user_account'],
|
|
118
|
-
),
|
|
119
|
-
tableFixture(
|
|
120
|
-
'public.user_profile',
|
|
121
|
-
buildUserProfiles(),
|
|
122
|
-
tableSchemas['public.user_profile'],
|
|
123
|
-
),
|
|
124
|
-
];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
describeUserProfile(suiteTitle, () => {
|
|
128
|
-
test('listUserProfiles hydrates optional profiles', async () => {
|
|
129
|
-
const fixtures = buildFixtures();
|
|
130
|
-
const provider = await getProvider();
|
|
131
|
-
await provider.withRepositoryFixture(fixtures, async (client) => {
|
|
132
|
-
const result = await listUserProfiles(client);
|
|
133
|
-
expect(result).toEqual([
|
|
134
|
-
{
|
|
135
|
-
userAccountId: 1,
|
|
136
|
-
username: 'alpha',
|
|
137
|
-
email: 'alpha@example.com',
|
|
138
|
-
displayName: 'Alpha Tester',
|
|
139
|
-
createdAt: new Date('2025-12-01T08:00:00Z'),
|
|
140
|
-
updatedAt: new Date('2025-12-01T09:00:00Z'),
|
|
141
|
-
profile: {
|
|
142
|
-
profileId: 101,
|
|
143
|
-
userAccountId: 1,
|
|
144
|
-
bio: 'Lead engineer and mapper advocate.',
|
|
145
|
-
website: 'https://example.com',
|
|
146
|
-
verified: true,
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
userAccountId: 2,
|
|
151
|
-
username: 'bravo',
|
|
152
|
-
email: 'bravo@example.com',
|
|
153
|
-
displayName: 'Bravo Builder',
|
|
154
|
-
createdAt: new Date('2025-12-02T10:00:00Z'),
|
|
155
|
-
updatedAt: new Date('2025-12-02T11:00:00Z'),
|
|
156
|
-
profile: undefined,
|
|
157
|
-
},
|
|
158
|
-
]);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
});
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import { tableSchemas } from './generated/ztd-row-map.generated';
|
|
3
|
-
import { userAccountWriterColumnSets } from '../src/repositories/user-accounts';
|
|
4
|
-
|
|
5
|
-
const userColumns = new Set(
|
|
6
|
-
Object.keys(tableSchemas['public.user_account'].columns),
|
|
7
|
-
);
|
|
8
|
-
|
|
9
|
-
describe('user_account writer columns', () => {
|
|
10
|
-
test('insert columns must exist on the canonical table', () => {
|
|
11
|
-
const { insertColumns } = userAccountWriterColumnSets;
|
|
12
|
-
const missing = insertColumns.filter((column) => !userColumns.has(column));
|
|
13
|
-
expect(missing, `Missing columns: ${missing.join(', ')}`).toEqual([]);
|
|
14
|
-
expect(insertColumns).toEqual(
|
|
15
|
-
expect.arrayContaining(['username', 'email', 'display_name']),
|
|
16
|
-
);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('writer column sets align with the canonical table', () => {
|
|
20
|
-
const { updateColumns, immutableColumns } = userAccountWriterColumnSets;
|
|
21
|
-
const missingUpdates = updateColumns.filter((column) => !userColumns.has(column));
|
|
22
|
-
expect(missingUpdates, `Missing update columns: ${missingUpdates.join(', ')}`).toEqual([]);
|
|
23
|
-
const missingImmutables = immutableColumns.filter((column) => !userColumns.has(column));
|
|
24
|
-
expect(missingImmutables, `Missing immutable columns: ${missingImmutables.join(', ')}`).toEqual([]);
|
|
25
|
-
immutableColumns.forEach((column) => {
|
|
26
|
-
expect(
|
|
27
|
-
updateColumns,
|
|
28
|
-
`Immutable column "${column}" should never appear in updateColumns`,
|
|
29
|
-
).not.toContain(column);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
});
|