schematic-pg 0.1.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.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1019 -0
  3. package/dist/api/auth/errors.d.ts +6 -0
  4. package/dist/api/auth/errors.js +12 -0
  5. package/dist/api/auth/jwt-resolver.d.ts +7 -0
  6. package/dist/api/auth/jwt-resolver.js +59 -0
  7. package/dist/api/auth/middleware.d.ts +4 -0
  8. package/dist/api/auth/middleware.js +10 -0
  9. package/dist/api/auth/policy.d.ts +7 -0
  10. package/dist/api/auth/policy.js +95 -0
  11. package/dist/api/auth/template.d.ts +2 -0
  12. package/dist/api/auth/template.js +24 -0
  13. package/dist/api/auth/types.d.ts +12 -0
  14. package/dist/api/auth/types.js +1 -0
  15. package/dist/api/middleware/db.d.ts +8 -0
  16. package/dist/api/middleware/db.js +12 -0
  17. package/dist/api/middleware/errors.d.ts +5 -0
  18. package/dist/api/middleware/errors.js +27 -0
  19. package/dist/api/middleware/validate.d.ts +23 -0
  20. package/dist/api/middleware/validate.js +13 -0
  21. package/dist/api/types.d.ts +8 -0
  22. package/dist/api/types.js +1 -0
  23. package/dist/api/utils/route-naming.d.ts +3 -0
  24. package/dist/api/utils/route-naming.js +25 -0
  25. package/dist/api-generator/app-generator.d.ts +11 -0
  26. package/dist/api-generator/app-generator.js +79 -0
  27. package/dist/api-generator/custom-route-scanner.d.ts +6 -0
  28. package/dist/api-generator/custom-route-scanner.js +42 -0
  29. package/dist/api-generator/generate-api-cli.d.ts +1 -0
  30. package/dist/api-generator/generate-api-cli.js +28 -0
  31. package/dist/api-generator/index.d.ts +11 -0
  32. package/dist/api-generator/index.js +15 -0
  33. package/dist/api-generator/policy-generator.d.ts +9 -0
  34. package/dist/api-generator/policy-generator.js +33 -0
  35. package/dist/api-generator/route-generator.d.ts +20 -0
  36. package/dist/api-generator/route-generator.js +198 -0
  37. package/dist/api-generator/utils/policy.d.ts +18 -0
  38. package/dist/api-generator/utils/policy.js +72 -0
  39. package/dist/api-generator/zod-schema-generator.d.ts +16 -0
  40. package/dist/api-generator/zod-schema-generator.js +145 -0
  41. package/dist/cli/db.d.ts +4 -0
  42. package/dist/cli/db.js +144 -0
  43. package/dist/cli/dev.d.ts +1 -0
  44. package/dist/cli/dev.js +10 -0
  45. package/dist/cli/generate.d.ts +4 -0
  46. package/dist/cli/generate.js +50 -0
  47. package/dist/cli/init.d.ts +1 -0
  48. package/dist/cli/init.js +57 -0
  49. package/dist/cli/paths.d.ts +5 -0
  50. package/dist/cli/paths.js +10 -0
  51. package/dist/cli/templates.d.ts +6 -0
  52. package/dist/cli/templates.js +85 -0
  53. package/dist/cli.d.ts +2 -0
  54. package/dist/cli.js +81 -0
  55. package/dist/constants.d.ts +1 -0
  56. package/dist/constants.js +1 -0
  57. package/dist/db/bootstrap-cli.d.ts +1 -0
  58. package/dist/db/bootstrap-cli.js +17 -0
  59. package/dist/db/bootstrap.d.ts +3 -0
  60. package/dist/db/bootstrap.js +16 -0
  61. package/dist/db/cli.d.ts +1 -0
  62. package/dist/db/cli.js +20 -0
  63. package/dist/db/client.d.ts +8 -0
  64. package/dist/db/client.js +23 -0
  65. package/dist/db/config.d.ts +1 -0
  66. package/dist/db/config.js +10 -0
  67. package/dist/db/db-client-generator.d.ts +13 -0
  68. package/dist/db/db-client-generator.js +70 -0
  69. package/dist/db/diff-cli.d.ts +1 -0
  70. package/dist/db/diff-cli.js +46 -0
  71. package/dist/db/diff.d.ts +9 -0
  72. package/dist/db/diff.js +30 -0
  73. package/dist/db/errors.d.ts +34 -0
  74. package/dist/db/errors.js +88 -0
  75. package/dist/db/generate-client-cli.d.ts +1 -0
  76. package/dist/db/generate-client-cli.js +21 -0
  77. package/dist/db/index.d.ts +19 -0
  78. package/dist/db/index.js +17 -0
  79. package/dist/db/load-env.d.ts +3 -0
  80. package/dist/db/load-env.js +19 -0
  81. package/dist/db/migrate-cli.d.ts +1 -0
  82. package/dist/db/migrate-cli.js +88 -0
  83. package/dist/db/migrate.d.ts +3 -0
  84. package/dist/db/migrate.js +32 -0
  85. package/dist/db/migrations.d.ts +17 -0
  86. package/dist/db/migrations.js +81 -0
  87. package/dist/db/model-client.d.ts +36 -0
  88. package/dist/db/model-client.js +83 -0
  89. package/dist/db/model-meta.d.ts +36 -0
  90. package/dist/db/model-meta.js +57 -0
  91. package/dist/db/query-builder.d.ts +33 -0
  92. package/dist/db/query-builder.js +97 -0
  93. package/dist/db/reset-database.d.ts +4 -0
  94. package/dist/db/reset-database.js +9 -0
  95. package/dist/db/row-mapper.d.ts +3 -0
  96. package/dist/db/row-mapper.js +41 -0
  97. package/dist/db/schema-state.d.ts +7 -0
  98. package/dist/db/schema-state.js +32 -0
  99. package/dist/db/type-generator.d.ts +19 -0
  100. package/dist/db/type-generator.js +136 -0
  101. package/dist/db/utils/naming.d.ts +6 -0
  102. package/dist/db/utils/naming.js +23 -0
  103. package/dist/db/where-translator.d.ts +20 -0
  104. package/dist/db/where-translator.js +141 -0
  105. package/dist/index.d.ts +1 -0
  106. package/dist/index.js +1 -0
  107. package/dist/routes/health.d.ts +4 -0
  108. package/dist/routes/health.js +4 -0
  109. package/dist/schema-dsl/ast.d.ts +108 -0
  110. package/dist/schema-dsl/ast.js +1 -0
  111. package/dist/schema-dsl/cli.d.ts +1 -0
  112. package/dist/schema-dsl/cli.js +25 -0
  113. package/dist/schema-dsl/index.d.ts +8 -0
  114. package/dist/schema-dsl/index.js +16 -0
  115. package/dist/schema-dsl/inspect.d.ts +1 -0
  116. package/dist/schema-dsl/inspect.js +9 -0
  117. package/dist/schema-dsl/lexer.d.ts +31 -0
  118. package/dist/schema-dsl/lexer.js +216 -0
  119. package/dist/schema-dsl/parser.d.ts +49 -0
  120. package/dist/schema-dsl/parser.js +372 -0
  121. package/dist/schema-dsl/tokens.d.ts +30 -0
  122. package/dist/schema-dsl/tokens.js +35 -0
  123. package/dist/sql-generator/cli.d.ts +1 -0
  124. package/dist/sql-generator/cli.js +7 -0
  125. package/dist/sql-generator/generators/drop-tables.d.ts +2 -0
  126. package/dist/sql-generator/generators/drop-tables.js +8 -0
  127. package/dist/sql-generator/generators/enums.d.ts +4 -0
  128. package/dist/sql-generator/generators/enums.js +16 -0
  129. package/dist/sql-generator/generators/extensions.d.ts +4 -0
  130. package/dist/sql-generator/generators/extensions.js +11 -0
  131. package/dist/sql-generator/generators/foreign-keys.d.ts +4 -0
  132. package/dist/sql-generator/generators/foreign-keys.js +23 -0
  133. package/dist/sql-generator/generators/indexes.d.ts +13 -0
  134. package/dist/sql-generator/generators/indexes.js +39 -0
  135. package/dist/sql-generator/generators/tables.d.ts +4 -0
  136. package/dist/sql-generator/generators/tables.js +65 -0
  137. package/dist/sql-generator/generators/triggers.d.ts +6 -0
  138. package/dist/sql-generator/generators/triggers.js +47 -0
  139. package/dist/sql-generator/index.d.ts +5 -0
  140. package/dist/sql-generator/index.js +3 -0
  141. package/dist/sql-generator/migration-planner.d.ts +15 -0
  142. package/dist/sql-generator/migration-planner.js +207 -0
  143. package/dist/sql-generator/migration-sql-generator.d.ts +9 -0
  144. package/dist/sql-generator/migration-sql-generator.js +181 -0
  145. package/dist/sql-generator/migration-types.d.ts +86 -0
  146. package/dist/sql-generator/migration-types.js +1 -0
  147. package/dist/sql-generator/sql-generator.d.ts +6 -0
  148. package/dist/sql-generator/sql-generator.js +26 -0
  149. package/dist/sql-generator/utils/ast-helpers.d.ts +58 -0
  150. package/dist/sql-generator/utils/ast-helpers.js +252 -0
  151. package/dist/sql-generator/utils/format.d.ts +2 -0
  152. package/dist/sql-generator/utils/format.js +21 -0
  153. package/dist/sql-generator/utils/snake-case.d.ts +3 -0
  154. package/dist/sql-generator/utils/snake-case.js +96 -0
  155. package/dist/sql-generator/utils/type-mapper.d.ts +2 -0
  156. package/dist/sql-generator/utils/type-mapper.js +39 -0
  157. package/dist/sql-generator/utils/value-formatter.d.ts +4 -0
  158. package/dist/sql-generator/utils/value-formatter.js +41 -0
  159. package/dist/types/generated-db.stub.d.ts +2 -0
  160. package/dist/types/generated-db.stub.js +3 -0
  161. package/dist/types/generated-policies.stub.d.ts +7 -0
  162. package/dist/types/generated-policies.stub.js +1 -0
  163. package/package.json +86 -0
@@ -0,0 +1,252 @@
1
+ import { formatDefaultValue, serializeValue } from './value-formatter.js';
2
+ import { mapColumnType } from './type-mapper.js';
3
+ import { toSnakeCase, toTableName } from './snake-case.js';
4
+ export function getModelNames(schema) {
5
+ return new Set(schema.models.map((model) => model.name));
6
+ }
7
+ export function getEnumNames(schema) {
8
+ return new Set(schema.enums.map((enumDef) => enumDef.name));
9
+ }
10
+ export function isStoredField(field, modelNames) {
11
+ return !modelNames.has(field.type.name);
12
+ }
13
+ export function getStoredFields(model, modelNames) {
14
+ return model.fields.filter((field) => isStoredField(field, modelNames));
15
+ }
16
+ export function getFieldAttribute(field, name) {
17
+ return field.attributes.find((attr) => attr.name === name);
18
+ }
19
+ export function getModelAttribute(model, name) {
20
+ return model.attributes.find((attr) => attr.name === name);
21
+ }
22
+ export function getDirective(model, name) {
23
+ return model.directives.find((directive) => directive.name === name);
24
+ }
25
+ export function getDirectives(model, name) {
26
+ return model.directives.filter((directive) => directive.name === name);
27
+ }
28
+ export function assertKeyValueArgs(args) {
29
+ if (!args || args.kind !== 'KeyValueArgs') {
30
+ throw new Error('Expected KeyValueArgs');
31
+ }
32
+ return args;
33
+ }
34
+ export function getKvPair(args, key) {
35
+ const pair = args.pairs.find((item) => item.key === key);
36
+ if (!pair) {
37
+ throw new Error(`Missing key "${key}" in KeyValueArgs`);
38
+ }
39
+ return pair;
40
+ }
41
+ export function getOptionalKvPair(args, key) {
42
+ return args.pairs.find((item) => item.key === key);
43
+ }
44
+ export function getIdentifierNames(value) {
45
+ if (value.kind !== 'ArrayLiteral') {
46
+ throw new Error('Expected ArrayLiteral');
47
+ }
48
+ return value.elements.map((element) => {
49
+ if (element.kind !== 'Identifier') {
50
+ throw new Error('Expected Identifier in array');
51
+ }
52
+ return element.name;
53
+ });
54
+ }
55
+ export function fieldHasAttribute(field, name) {
56
+ return field.attributes.some((attr) => attr.name === name);
57
+ }
58
+ export function getPrimaryKey(model) {
59
+ const compositeDirective = getDirective(model, 'id');
60
+ if (compositeDirective?.args?.kind === 'KeyValueArgs') {
61
+ const fields = getIdentifierNames(getKvPair(compositeDirective.args, 'fields').value);
62
+ return { fields, composite: fields.length > 1 };
63
+ }
64
+ const modelLevelId = getModelAttribute(model, 'id');
65
+ if (modelLevelId) {
66
+ const idField = model.fields.find((field) => field.name === 'id');
67
+ if (idField) {
68
+ return { fields: [idField.name], composite: false };
69
+ }
70
+ }
71
+ const idFields = model.fields.filter((field) => fieldHasAttribute(field, 'id')).map((field) => field.name);
72
+ if (idFields.length === 1) {
73
+ return { fields: idFields, composite: false };
74
+ }
75
+ if (idFields.length > 1) {
76
+ return { fields: idFields, composite: true };
77
+ }
78
+ return undefined;
79
+ }
80
+ export function getDefaultExpression(field, enumNames) {
81
+ const defaultAttr = getFieldAttribute(field, 'default');
82
+ if (!defaultAttr?.args || defaultAttr.args.kind !== 'ExpressionArgs') {
83
+ return undefined;
84
+ }
85
+ const [expression] = defaultAttr.args.expressions;
86
+ if (!expression) {
87
+ return undefined;
88
+ }
89
+ return formatDefaultValue(expression, field.type, enumNames);
90
+ }
91
+ export function collectValidationComments(field) {
92
+ const comments = [];
93
+ const regexAttr = getFieldAttribute(field, 'regex');
94
+ if (regexAttr?.args?.kind === 'KeyValueArgs') {
95
+ const pattern = getOptionalKvPair(regexAttr.args, 'pattern');
96
+ const message = getOptionalKvPair(regexAttr.args, 'message');
97
+ const patternValue = pattern?.value.kind === 'StringLiteral' ? `'${pattern.value.value}'` : 'unknown';
98
+ const messageValue = message?.value.kind === 'StringLiteral' ? `'${message.value.value}'` : 'unknown';
99
+ comments.push(`-- @regex: pattern = ${patternValue}, message = ${messageValue}`);
100
+ }
101
+ const rangeAttr = getFieldAttribute(field, 'range');
102
+ if (rangeAttr?.args?.kind === 'KeyValueArgs') {
103
+ const min = getOptionalKvPair(rangeAttr.args, 'min');
104
+ const max = getOptionalKvPair(rangeAttr.args, 'max');
105
+ const message = getOptionalKvPair(rangeAttr.args, 'message');
106
+ const minValue = min?.value.kind === 'NumberLiteral' ? String(min.value.value) : 'unknown';
107
+ const maxValue = max?.value.kind === 'NumberLiteral' ? String(max.value.value) : 'unknown';
108
+ const messageValue = message?.value.kind === 'StringLiteral' ? `'${message.value.value}'` : 'unknown';
109
+ comments.push(`-- @range: min = ${minValue}, max = ${maxValue}, message = ${messageValue}`);
110
+ }
111
+ return comments;
112
+ }
113
+ export function mapReferentialAction(value) {
114
+ if (!value || value.kind !== 'Identifier') {
115
+ return undefined;
116
+ }
117
+ return value.name.replace(/_/g, ' ');
118
+ }
119
+ export function collectForeignKeys(schema) {
120
+ const modelNames = getModelNames(schema);
121
+ const foreignKeys = [];
122
+ for (const model of schema.models) {
123
+ for (const field of model.fields) {
124
+ if (!modelNames.has(field.type.name)) {
125
+ continue;
126
+ }
127
+ const relation = getFieldAttribute(field, 'relation');
128
+ if (!relation?.args || relation.args.kind !== 'KeyValueArgs') {
129
+ continue;
130
+ }
131
+ const fieldsPair = getOptionalKvPair(relation.args, 'fields');
132
+ const referencesPair = getOptionalKvPair(relation.args, 'references');
133
+ if (!fieldsPair || !referencesPair) {
134
+ continue;
135
+ }
136
+ const sourceColumns = getIdentifierNames(fieldsPair.value);
137
+ const targetColumns = getIdentifierNames(referencesPair.value);
138
+ const onDelete = mapReferentialAction(getOptionalKvPair(relation.args, 'onDelete')?.value);
139
+ const onUpdate = mapReferentialAction(getOptionalKvPair(relation.args, 'onUpdate')?.value);
140
+ foreignKeys.push({
141
+ sourceModel: model.name,
142
+ sourceTable: toTableName(model.name),
143
+ sourceColumns: sourceColumns.map(toSnakeCase),
144
+ targetModel: field.type.name,
145
+ targetTable: toTableName(field.type.name),
146
+ targetColumns: targetColumns.map(toSnakeCase),
147
+ onDelete,
148
+ onUpdate,
149
+ });
150
+ }
151
+ }
152
+ return foreignKeys;
153
+ }
154
+ export function serializeColumnType(type, enumNames) {
155
+ return mapColumnType(type, enumNames);
156
+ }
157
+ export function serializeDefault(field, enumNames) {
158
+ const defaultAttr = getFieldAttribute(field, 'default');
159
+ if (!defaultAttr?.args || defaultAttr.args.kind !== 'ExpressionArgs') {
160
+ return undefined;
161
+ }
162
+ const [expression] = defaultAttr.args.expressions;
163
+ if (!expression) {
164
+ return undefined;
165
+ }
166
+ return serializeValue(expression);
167
+ }
168
+ export function getFieldSnakeNameMap(model, modelNames) {
169
+ const map = new Map();
170
+ for (const field of getStoredFields(model, modelNames)) {
171
+ map.set(field.name, toSnakeCase(field.name));
172
+ }
173
+ return map;
174
+ }
175
+ export function transformWhereClause(where, fieldNameMap) {
176
+ let transformed = where;
177
+ const sortedFieldNames = [...fieldNameMap.keys()].sort((a, b) => b.length - a.length);
178
+ for (const fieldName of sortedFieldNames) {
179
+ const snakeName = fieldNameMap.get(fieldName);
180
+ const pattern = new RegExp(`\\b${escapeRegExp(fieldName)}\\b`, 'g');
181
+ transformed = transformed.replace(pattern, snakeName);
182
+ }
183
+ return transformed;
184
+ }
185
+ function escapeRegExp(value) {
186
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
187
+ }
188
+ export function serializeForeignKey(foreignKey) {
189
+ return JSON.stringify({
190
+ sourceTable: foreignKey.sourceTable,
191
+ sourceColumns: foreignKey.sourceColumns,
192
+ targetTable: foreignKey.targetTable,
193
+ targetColumns: foreignKey.targetColumns,
194
+ onDelete: foreignKey.onDelete,
195
+ onUpdate: foreignKey.onUpdate,
196
+ });
197
+ }
198
+ export function parseForeignKeySignature(signature) {
199
+ const parsed = JSON.parse(signature);
200
+ return {
201
+ sourceModel: '',
202
+ targetModel: '',
203
+ ...parsed,
204
+ };
205
+ }
206
+ export function normalizeIndexDirective(directive, model, modelNames) {
207
+ const args = assertKeyValueArgs(directive.args);
208
+ const fields = getIdentifierNames(getKvPair(args, 'fields').value);
209
+ const wherePair = getOptionalKvPair(args, 'where');
210
+ const uniquePair = getOptionalKvPair(args, 'unique');
211
+ const namePair = getOptionalKvPair(args, 'name');
212
+ const typePair = getOptionalKvPair(args, 'type');
213
+ const fieldNameMap = getFieldSnakeNameMap(model, modelNames);
214
+ return {
215
+ fields,
216
+ where: wherePair?.value.kind === 'StringLiteral'
217
+ ? transformWhereClause(wherePair.value.value, fieldNameMap)
218
+ : undefined,
219
+ unique: uniquePair?.value.kind === 'BooleanLiteral' ? uniquePair.value.value : undefined,
220
+ name: namePair?.value.kind === 'StringLiteral' ? namePair.value.value : undefined,
221
+ type: typePair?.value.kind === 'Identifier' ? typePair.value.name : undefined,
222
+ };
223
+ }
224
+ export function normalizeTriggerDirective(directive) {
225
+ const args = assertKeyValueArgs(directive.args);
226
+ const timing = getKvPair(args, 'timing').value;
227
+ const event = getKvPair(args, 'event').value;
228
+ const execute = getKvPair(args, 'execute').value;
229
+ const levelPair = getOptionalKvPair(args, 'level');
230
+ if (timing.kind !== 'Identifier' || event.kind !== 'Identifier') {
231
+ throw new Error('Trigger timing and event must be identifiers');
232
+ }
233
+ if (execute.kind !== 'TripleStringLiteral') {
234
+ throw new Error('Trigger execute must be a triple-quoted string');
235
+ }
236
+ const levelValue = levelPair?.value.kind === 'Identifier' ? levelPair.value.name.toUpperCase() : 'ROW';
237
+ return {
238
+ timing: timing.name.toUpperCase(),
239
+ event: event.name.toUpperCase(),
240
+ level: levelValue,
241
+ execute: execute.value.trim(),
242
+ };
243
+ }
244
+ export function resolveTriggerNames(model, timing, event) {
245
+ const timingValue = timing.toLowerCase();
246
+ const eventValue = event.toLowerCase();
247
+ const baseName = `${toTableName(model.name)}_${timingValue}_${eventValue}`;
248
+ return {
249
+ functionName: `${baseName}_trigger_func`,
250
+ triggerName: `${baseName}_trigger`,
251
+ };
252
+ }
@@ -0,0 +1,2 @@
1
+ export declare function joinSection(header: string, statements: string[]): string;
2
+ export declare function formatCreateTable(tableName: string, blocks: string[][]): string;
@@ -0,0 +1,21 @@
1
+ export function joinSection(header, statements) {
2
+ if (statements.length === 0) {
3
+ return `-- ${header}\n`;
4
+ }
5
+ return `-- ${header}\n\n${statements.join('\n\n')}\n`;
6
+ }
7
+ export function formatCreateTable(tableName, blocks) {
8
+ const renderedBlocks = blocks.map((lines, blockIndex) => {
9
+ const isLastBlock = blockIndex === blocks.length - 1;
10
+ const renderedLines = lines.map((line, lineIndex) => {
11
+ const prefix = ' ';
12
+ if (line.startsWith('--')) {
13
+ return `${prefix}${line}`;
14
+ }
15
+ const needsComma = lineIndex === 0 && !isLastBlock;
16
+ return `${prefix}${line}${needsComma ? ',' : ''}`;
17
+ });
18
+ return renderedLines.join('\n');
19
+ });
20
+ return `CREATE TABLE ${tableName} (\n${renderedBlocks.join('\n')}\n);`;
21
+ }
@@ -0,0 +1,3 @@
1
+ export declare function toSnakeCase(str: string): string;
2
+ export declare function toTableName(modelName: string): string;
3
+ export declare function quoteIdentifier(name: string): string;
@@ -0,0 +1,96 @@
1
+ const POSTGRES_RESERVED_WORDS = new Set([
2
+ 'all',
3
+ 'analyse',
4
+ 'analyze',
5
+ 'and',
6
+ 'any',
7
+ 'array',
8
+ 'as',
9
+ 'asc',
10
+ 'asymmetric',
11
+ 'both',
12
+ 'case',
13
+ 'cast',
14
+ 'check',
15
+ 'collate',
16
+ 'column',
17
+ 'constraint',
18
+ 'create',
19
+ 'current_catalog',
20
+ 'current_date',
21
+ 'current_role',
22
+ 'current_time',
23
+ 'current_timestamp',
24
+ 'current_user',
25
+ 'default',
26
+ 'deferrable',
27
+ 'desc',
28
+ 'distinct',
29
+ 'do',
30
+ 'else',
31
+ 'end',
32
+ 'except',
33
+ 'false',
34
+ 'fetch',
35
+ 'for',
36
+ 'foreign',
37
+ 'from',
38
+ 'grant',
39
+ 'group',
40
+ 'having',
41
+ 'in',
42
+ 'initially',
43
+ 'intersect',
44
+ 'into',
45
+ 'leading',
46
+ 'limit',
47
+ 'localtime',
48
+ 'localtimestamp',
49
+ 'not',
50
+ 'null',
51
+ 'offset',
52
+ 'on',
53
+ 'only',
54
+ 'or',
55
+ 'order',
56
+ 'placing',
57
+ 'primary',
58
+ 'references',
59
+ 'returning',
60
+ 'select',
61
+ 'session_user',
62
+ 'some',
63
+ 'symmetric',
64
+ 'table',
65
+ 'then',
66
+ 'to',
67
+ 'trailing',
68
+ 'true',
69
+ 'union',
70
+ 'unique',
71
+ 'user',
72
+ 'using',
73
+ 'variadic',
74
+ 'when',
75
+ 'where',
76
+ 'window',
77
+ 'with',
78
+ ]);
79
+ export function toSnakeCase(str) {
80
+ return str
81
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
82
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
83
+ .toLowerCase();
84
+ }
85
+ export function toTableName(modelName) {
86
+ return toSnakeCase(modelName);
87
+ }
88
+ export function quoteIdentifier(name) {
89
+ if (POSTGRES_RESERVED_WORDS.has(name.toLowerCase())) {
90
+ return `"${name.replace(/"/g, '""')}"`;
91
+ }
92
+ if (/^[a-z_][a-z0-9_]*$/.test(name)) {
93
+ return name;
94
+ }
95
+ return `"${name.replace(/"/g, '""')}"`;
96
+ }
@@ -0,0 +1,2 @@
1
+ import type { TypeExpr } from '../../schema-dsl/ast.js';
2
+ export declare function mapColumnType(type: TypeExpr, enumNames: Set<string>): string;
@@ -0,0 +1,39 @@
1
+ import { toSnakeCase } from './snake-case.js';
2
+ const PRIMITIVE_TYPES = new Set([
3
+ 'UUID',
4
+ 'TEXT',
5
+ 'INTEGER',
6
+ 'SERIAL',
7
+ 'BOOLEAN',
8
+ 'JSONB',
9
+ 'POINT',
10
+ 'SMALLINT',
11
+ 'VARCHAR',
12
+ 'DECIMAL',
13
+ 'TIMESTAMP',
14
+ ]);
15
+ export function mapColumnType(type, enumNames) {
16
+ const baseType = mapBaseType(type, enumNames);
17
+ return type.array ? `${baseType}[]` : baseType;
18
+ }
19
+ function mapBaseType(type, enumNames) {
20
+ const { name } = type;
21
+ if (enumNames.has(name)) {
22
+ return toSnakeCase(name);
23
+ }
24
+ if (name === 'TIMESTAMP') {
25
+ return 'TIMESTAMP WITH TIME ZONE';
26
+ }
27
+ if (name === 'VARCHAR' && type.args?.length) {
28
+ const length = type.args.map((arg) => arg.value).join(', ');
29
+ return `VARCHAR(${length})`;
30
+ }
31
+ if (name === 'DECIMAL' && type.args?.length) {
32
+ const args = type.args.map((arg) => arg.value).join(', ');
33
+ return `DECIMAL(${args})`;
34
+ }
35
+ if (PRIMITIVE_TYPES.has(name)) {
36
+ return name;
37
+ }
38
+ return name;
39
+ }
@@ -0,0 +1,4 @@
1
+ import type { TypeExpr, Value } from '../../schema-dsl/ast.js';
2
+ export declare function formatDefaultValue(value: Value, columnType: TypeExpr, enumNames: Set<string>): string;
3
+ export declare function escapeSqlString(value: string): string;
4
+ export declare function serializeValue(value: Value): string;
@@ -0,0 +1,41 @@
1
+ export function formatDefaultValue(value, columnType, enumNames) {
2
+ switch (value.kind) {
3
+ case 'CallExpression': {
4
+ const args = value.args.map((arg) => formatValueLiteral(arg, enumNames)).join(', ');
5
+ return args.length > 0 ? `${value.callee}(${args})` : `${value.callee}()`;
6
+ }
7
+ case 'BooleanLiteral':
8
+ return value.value ? 'true' : 'false';
9
+ case 'NumberLiteral':
10
+ return String(value.value);
11
+ case 'Identifier':
12
+ if (enumNames.has(columnType.name)) {
13
+ return `'${escapeSqlString(value.name)}'`;
14
+ }
15
+ return value.name;
16
+ case 'StringLiteral':
17
+ return `'${escapeSqlString(value.value)}'`;
18
+ default:
19
+ throw new Error(`Unsupported default value kind: ${value.kind}`);
20
+ }
21
+ }
22
+ function formatValueLiteral(value, enumNames) {
23
+ switch (value.kind) {
24
+ case 'BooleanLiteral':
25
+ return value.value ? 'true' : 'false';
26
+ case 'NumberLiteral':
27
+ return String(value.value);
28
+ case 'Identifier':
29
+ return value.name;
30
+ case 'StringLiteral':
31
+ return `'${escapeSqlString(value.value)}'`;
32
+ default:
33
+ throw new Error(`Unsupported default argument kind: ${value.kind}`);
34
+ }
35
+ }
36
+ export function escapeSqlString(value) {
37
+ return value.replace(/'/g, "''");
38
+ }
39
+ export function serializeValue(value) {
40
+ return JSON.stringify(value);
41
+ }
@@ -0,0 +1,2 @@
1
+ export type DbClient = Record<string, unknown>;
2
+ export declare function createDbClient(_pool: unknown): DbClient;
@@ -0,0 +1,3 @@
1
+ export function createDbClient(_pool) {
2
+ return {};
3
+ }
@@ -0,0 +1,7 @@
1
+ export type PolicyOperation = 'select' | 'insert' | 'update' | 'delete';
2
+ export interface NormalizedPolicy {
3
+ role: string;
4
+ operations: PolicyOperation[] | 'all';
5
+ where?: string;
6
+ }
7
+ export declare const POLICIES: Record<string, NormalizedPolicy[]>;
@@ -0,0 +1 @@
1
+ export const POLICIES = {};
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "schematic-pg",
3
+ "version": "0.1.0",
4
+ "description": "Single-file backend framework for PostgreSQL and Node.js",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=20"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/tawsbob/postgrest.js.git"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "bin": {
17
+ "schematic-pg": "./dist/cli.js"
18
+ },
19
+ "files": [
20
+ "dist/",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "workspaces": [
25
+ "editors/vscode",
26
+ "editors/language-server"
27
+ ],
28
+ "imports": {
29
+ "generated/db.js": "./generated/db.js",
30
+ "generated/policies.js": "./generated/policies.js"
31
+ },
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "import": "./dist/index.js"
36
+ },
37
+ "./schema-dsl": {
38
+ "types": "./dist/schema-dsl/index.d.ts",
39
+ "import": "./dist/schema-dsl/index.js"
40
+ },
41
+ "./api/*": {
42
+ "types": "./dist/api/*.d.ts",
43
+ "import": "./dist/api/*.js"
44
+ },
45
+ "./db/*": {
46
+ "types": "./dist/db/*.d.ts",
47
+ "import": "./dist/db/*.js"
48
+ }
49
+ },
50
+ "scripts": {
51
+ "build": "tsc -p tsconfig.build.json",
52
+ "parse": "tsx src/schema-dsl/cli.ts",
53
+ "tokenize": "tsx src/schema-dsl/cli.ts --tokens",
54
+ "generate": "tsx src/cli.ts generate",
55
+ "generate:client": "tsx src/cli.ts generate:client",
56
+ "generate:api": "tsx src/cli.ts generate:api",
57
+ "dev:api": "tsx src/cli.ts dev",
58
+ "setup:env": "test -f .env || cp .env.example .env",
59
+ "db:ping": "tsx src/cli.ts db:ping",
60
+ "db:bootstrap": "tsx src/cli.ts db:bootstrap",
61
+ "db:diff": "tsx src/cli.ts db:diff",
62
+ "db:migrate": "tsx src/cli.ts db:migrate",
63
+ "db:migrate:status": "tsx src/cli.ts db:migrate:status",
64
+ "build:lsp": "npm run build --workspace=@schematic-pg/schema-dsl-language-server",
65
+ "build:extension": "npm run vscode:prepublish --workspace=schematic-pg-schema-dsl-vscode && npm run package --workspace=schematic-pg-schema-dsl-vscode",
66
+ "test": "node --import tsx --test $(find src editors/language-server -path '*/__tests__/*.test.ts' ! -name '*.integration.test.ts')",
67
+ "test:integration": "npm run setup:env && docker compose up -d --wait && npm run generate:client && npm run generate:api && JWT_SECRET=integration-test-secret node --import tsx --test --test-concurrency=1 'src/**/__tests__/**/*.integration.test.ts'",
68
+ "docker:up": "docker compose up -d",
69
+ "docker:down": "docker compose down",
70
+ "docker:logs": "docker compose logs -f postgres",
71
+ "docker:reset": "docker compose down -v && rm -rf docker_data/postgres && docker compose up -d"
72
+ },
73
+ "dependencies": {
74
+ "@hono/node-server": "^2.0.6",
75
+ "@hono/zod-validator": "^0.8.0",
76
+ "hono": "^4.12.27",
77
+ "pg": "^8.22.0",
78
+ "tsx": "^4.19.4",
79
+ "zod": "^4.4.3"
80
+ },
81
+ "devDependencies": {
82
+ "@types/node": "^22.15.21",
83
+ "@types/pg": "^8.11.14",
84
+ "typescript": "^5.8.3"
85
+ }
86
+ }