@edge-base/cli 0.1.1

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 (234) hide show
  1. package/README.md +182 -0
  2. package/dist/commands/admin.d.ts +10 -0
  3. package/dist/commands/admin.d.ts.map +1 -0
  4. package/dist/commands/admin.js +307 -0
  5. package/dist/commands/admin.js.map +1 -0
  6. package/dist/commands/backup.d.ts +148 -0
  7. package/dist/commands/backup.d.ts.map +1 -0
  8. package/dist/commands/backup.js +1247 -0
  9. package/dist/commands/backup.js.map +1 -0
  10. package/dist/commands/completion.d.ts +3 -0
  11. package/dist/commands/completion.d.ts.map +1 -0
  12. package/dist/commands/completion.js +168 -0
  13. package/dist/commands/completion.js.map +1 -0
  14. package/dist/commands/create-plugin.d.ts +3 -0
  15. package/dist/commands/create-plugin.d.ts.map +1 -0
  16. package/dist/commands/create-plugin.js +208 -0
  17. package/dist/commands/create-plugin.js.map +1 -0
  18. package/dist/commands/deploy.d.ts +146 -0
  19. package/dist/commands/deploy.d.ts.map +1 -0
  20. package/dist/commands/deploy.js +1823 -0
  21. package/dist/commands/deploy.js.map +1 -0
  22. package/dist/commands/describe.d.ts +45 -0
  23. package/dist/commands/describe.d.ts.map +1 -0
  24. package/dist/commands/describe.js +114 -0
  25. package/dist/commands/describe.js.map +1 -0
  26. package/dist/commands/destroy.d.ts +13 -0
  27. package/dist/commands/destroy.d.ts.map +1 -0
  28. package/dist/commands/destroy.js +642 -0
  29. package/dist/commands/destroy.js.map +1 -0
  30. package/dist/commands/dev.d.ts +80 -0
  31. package/dist/commands/dev.d.ts.map +1 -0
  32. package/dist/commands/dev.js +1131 -0
  33. package/dist/commands/dev.js.map +1 -0
  34. package/dist/commands/docker.d.ts +22 -0
  35. package/dist/commands/docker.d.ts.map +1 -0
  36. package/dist/commands/docker.js +373 -0
  37. package/dist/commands/docker.js.map +1 -0
  38. package/dist/commands/export.d.ts +15 -0
  39. package/dist/commands/export.d.ts.map +1 -0
  40. package/dist/commands/export.js +142 -0
  41. package/dist/commands/export.js.map +1 -0
  42. package/dist/commands/init.d.ts +7 -0
  43. package/dist/commands/init.d.ts.map +1 -0
  44. package/dist/commands/init.js +506 -0
  45. package/dist/commands/init.js.map +1 -0
  46. package/dist/commands/keys.d.ts +23 -0
  47. package/dist/commands/keys.d.ts.map +1 -0
  48. package/dist/commands/keys.js +347 -0
  49. package/dist/commands/keys.js.map +1 -0
  50. package/dist/commands/logs.d.ts +17 -0
  51. package/dist/commands/logs.d.ts.map +1 -0
  52. package/dist/commands/logs.js +104 -0
  53. package/dist/commands/logs.js.map +1 -0
  54. package/dist/commands/migrate.d.ts +29 -0
  55. package/dist/commands/migrate.d.ts.map +1 -0
  56. package/dist/commands/migrate.js +302 -0
  57. package/dist/commands/migrate.js.map +1 -0
  58. package/dist/commands/migration.d.ts +18 -0
  59. package/dist/commands/migration.d.ts.map +1 -0
  60. package/dist/commands/migration.js +114 -0
  61. package/dist/commands/migration.js.map +1 -0
  62. package/dist/commands/neon.d.ts +66 -0
  63. package/dist/commands/neon.d.ts.map +1 -0
  64. package/dist/commands/neon.js +600 -0
  65. package/dist/commands/neon.js.map +1 -0
  66. package/dist/commands/plugins.d.ts +9 -0
  67. package/dist/commands/plugins.d.ts.map +1 -0
  68. package/dist/commands/plugins.js +295 -0
  69. package/dist/commands/plugins.js.map +1 -0
  70. package/dist/commands/realtime.d.ts +3 -0
  71. package/dist/commands/realtime.d.ts.map +1 -0
  72. package/dist/commands/realtime.js +71 -0
  73. package/dist/commands/realtime.js.map +1 -0
  74. package/dist/commands/secret.d.ts +7 -0
  75. package/dist/commands/secret.d.ts.map +1 -0
  76. package/dist/commands/secret.js +180 -0
  77. package/dist/commands/secret.js.map +1 -0
  78. package/dist/commands/seed.d.ts +21 -0
  79. package/dist/commands/seed.d.ts.map +1 -0
  80. package/dist/commands/seed.js +325 -0
  81. package/dist/commands/seed.js.map +1 -0
  82. package/dist/commands/telemetry.d.ts +12 -0
  83. package/dist/commands/telemetry.d.ts.map +1 -0
  84. package/dist/commands/telemetry.js +57 -0
  85. package/dist/commands/telemetry.js.map +1 -0
  86. package/dist/commands/typegen.d.ts +26 -0
  87. package/dist/commands/typegen.d.ts.map +1 -0
  88. package/dist/commands/typegen.js +212 -0
  89. package/dist/commands/typegen.js.map +1 -0
  90. package/dist/commands/upgrade.d.ts +29 -0
  91. package/dist/commands/upgrade.d.ts.map +1 -0
  92. package/dist/commands/upgrade.js +265 -0
  93. package/dist/commands/upgrade.js.map +1 -0
  94. package/dist/commands/webhook-test.d.ts +3 -0
  95. package/dist/commands/webhook-test.d.ts.map +1 -0
  96. package/dist/commands/webhook-test.js +133 -0
  97. package/dist/commands/webhook-test.js.map +1 -0
  98. package/dist/index.d.ts +3 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +183 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/lib/agent-contract.d.ts +36 -0
  103. package/dist/lib/agent-contract.d.ts.map +1 -0
  104. package/dist/lib/agent-contract.js +78 -0
  105. package/dist/lib/agent-contract.js.map +1 -0
  106. package/dist/lib/cf-auth.d.ts +76 -0
  107. package/dist/lib/cf-auth.d.ts.map +1 -0
  108. package/dist/lib/cf-auth.js +321 -0
  109. package/dist/lib/cf-auth.js.map +1 -0
  110. package/dist/lib/cli-context.d.ts +23 -0
  111. package/dist/lib/cli-context.d.ts.map +1 -0
  112. package/dist/lib/cli-context.js +40 -0
  113. package/dist/lib/cli-context.js.map +1 -0
  114. package/dist/lib/cloudflare-deploy-manifest.d.ts +26 -0
  115. package/dist/lib/cloudflare-deploy-manifest.d.ts.map +1 -0
  116. package/dist/lib/cloudflare-deploy-manifest.js +107 -0
  117. package/dist/lib/cloudflare-deploy-manifest.js.map +1 -0
  118. package/dist/lib/cloudflare-wrangler-resources.d.ts +32 -0
  119. package/dist/lib/cloudflare-wrangler-resources.d.ts.map +1 -0
  120. package/dist/lib/cloudflare-wrangler-resources.js +59 -0
  121. package/dist/lib/cloudflare-wrangler-resources.js.map +1 -0
  122. package/dist/lib/config-editor.d.ts +139 -0
  123. package/dist/lib/config-editor.d.ts.map +1 -0
  124. package/dist/lib/config-editor.js +1188 -0
  125. package/dist/lib/config-editor.js.map +1 -0
  126. package/dist/lib/deploy-shared.d.ts +55 -0
  127. package/dist/lib/deploy-shared.d.ts.map +1 -0
  128. package/dist/lib/deploy-shared.js +183 -0
  129. package/dist/lib/deploy-shared.js.map +1 -0
  130. package/dist/lib/dev-sidecar.d.ts +31 -0
  131. package/dist/lib/dev-sidecar.d.ts.map +1 -0
  132. package/dist/lib/dev-sidecar.js +1058 -0
  133. package/dist/lib/dev-sidecar.js.map +1 -0
  134. package/dist/lib/fetch-with-timeout.d.ts +14 -0
  135. package/dist/lib/fetch-with-timeout.d.ts.map +1 -0
  136. package/dist/lib/fetch-with-timeout.js +29 -0
  137. package/dist/lib/fetch-with-timeout.js.map +1 -0
  138. package/dist/lib/function-registry.d.ts +56 -0
  139. package/dist/lib/function-registry.d.ts.map +1 -0
  140. package/dist/lib/function-registry.js +210 -0
  141. package/dist/lib/function-registry.js.map +1 -0
  142. package/dist/lib/load-config.d.ts +24 -0
  143. package/dist/lib/load-config.d.ts.map +1 -0
  144. package/dist/lib/load-config.js +263 -0
  145. package/dist/lib/load-config.js.map +1 -0
  146. package/dist/lib/local-secrets.d.ts +2 -0
  147. package/dist/lib/local-secrets.d.ts.map +1 -0
  148. package/dist/lib/local-secrets.js +60 -0
  149. package/dist/lib/local-secrets.js.map +1 -0
  150. package/dist/lib/managed-resource-names.d.ts +4 -0
  151. package/dist/lib/managed-resource-names.d.ts.map +1 -0
  152. package/dist/lib/managed-resource-names.js +19 -0
  153. package/dist/lib/managed-resource-names.js.map +1 -0
  154. package/dist/lib/migrator.d.ts +57 -0
  155. package/dist/lib/migrator.d.ts.map +1 -0
  156. package/dist/lib/migrator.js +321 -0
  157. package/dist/lib/migrator.js.map +1 -0
  158. package/dist/lib/neon.d.ts +41 -0
  159. package/dist/lib/neon.d.ts.map +1 -0
  160. package/dist/lib/neon.js +325 -0
  161. package/dist/lib/neon.js.map +1 -0
  162. package/dist/lib/node-tools.d.ts +10 -0
  163. package/dist/lib/node-tools.d.ts.map +1 -0
  164. package/dist/lib/node-tools.js +32 -0
  165. package/dist/lib/node-tools.js.map +1 -0
  166. package/dist/lib/npm.d.ts +8 -0
  167. package/dist/lib/npm.d.ts.map +1 -0
  168. package/dist/lib/npm.js +10 -0
  169. package/dist/lib/npm.js.map +1 -0
  170. package/dist/lib/npx.d.ts +9 -0
  171. package/dist/lib/npx.d.ts.map +1 -0
  172. package/dist/lib/npx.js +11 -0
  173. package/dist/lib/npx.js.map +1 -0
  174. package/dist/lib/project-runtime.d.ts +38 -0
  175. package/dist/lib/project-runtime.d.ts.map +1 -0
  176. package/dist/lib/project-runtime.js +122 -0
  177. package/dist/lib/project-runtime.js.map +1 -0
  178. package/dist/lib/prompts.d.ts +28 -0
  179. package/dist/lib/prompts.d.ts.map +1 -0
  180. package/dist/lib/prompts.js +85 -0
  181. package/dist/lib/prompts.js.map +1 -0
  182. package/dist/lib/rate-limit-bindings.d.ts +11 -0
  183. package/dist/lib/rate-limit-bindings.d.ts.map +1 -0
  184. package/dist/lib/rate-limit-bindings.js +52 -0
  185. package/dist/lib/rate-limit-bindings.js.map +1 -0
  186. package/dist/lib/realtime-provision.d.ts +22 -0
  187. package/dist/lib/realtime-provision.d.ts.map +1 -0
  188. package/dist/lib/realtime-provision.js +246 -0
  189. package/dist/lib/realtime-provision.js.map +1 -0
  190. package/dist/lib/resolve-options.d.ts +42 -0
  191. package/dist/lib/resolve-options.d.ts.map +1 -0
  192. package/dist/lib/resolve-options.js +98 -0
  193. package/dist/lib/resolve-options.js.map +1 -0
  194. package/dist/lib/runtime-scaffold.d.ts +17 -0
  195. package/dist/lib/runtime-scaffold.d.ts.map +1 -0
  196. package/dist/lib/runtime-scaffold.js +366 -0
  197. package/dist/lib/runtime-scaffold.js.map +1 -0
  198. package/dist/lib/schema-check.d.ts +79 -0
  199. package/dist/lib/schema-check.d.ts.map +1 -0
  200. package/dist/lib/schema-check.js +347 -0
  201. package/dist/lib/schema-check.js.map +1 -0
  202. package/dist/lib/spinner.d.ts +20 -0
  203. package/dist/lib/spinner.d.ts.map +1 -0
  204. package/dist/lib/spinner.js +42 -0
  205. package/dist/lib/spinner.js.map +1 -0
  206. package/dist/lib/telemetry.d.ts +37 -0
  207. package/dist/lib/telemetry.d.ts.map +1 -0
  208. package/dist/lib/telemetry.js +98 -0
  209. package/dist/lib/telemetry.js.map +1 -0
  210. package/dist/lib/turnstile-provision.d.ts +27 -0
  211. package/dist/lib/turnstile-provision.d.ts.map +1 -0
  212. package/dist/lib/turnstile-provision.js +144 -0
  213. package/dist/lib/turnstile-provision.js.map +1 -0
  214. package/dist/lib/update-check.d.ts +13 -0
  215. package/dist/lib/update-check.d.ts.map +1 -0
  216. package/dist/lib/update-check.js +110 -0
  217. package/dist/lib/update-check.js.map +1 -0
  218. package/dist/lib/wrangler-secrets.d.ts +3 -0
  219. package/dist/lib/wrangler-secrets.d.ts.map +1 -0
  220. package/dist/lib/wrangler-secrets.js +32 -0
  221. package/dist/lib/wrangler-secrets.js.map +1 -0
  222. package/dist/lib/wrangler.d.ts +9 -0
  223. package/dist/lib/wrangler.d.ts.map +1 -0
  224. package/dist/lib/wrangler.js +84 -0
  225. package/dist/lib/wrangler.js.map +1 -0
  226. package/dist/templates/plugin/README.md.tmpl +91 -0
  227. package/dist/templates/plugin/client/js/package.json.tmpl +23 -0
  228. package/dist/templates/plugin/client/js/src/index.ts.tmpl +68 -0
  229. package/dist/templates/plugin/client/js/tsconfig.json.tmpl +14 -0
  230. package/dist/templates/plugin/server/package.json.tmpl +19 -0
  231. package/dist/templates/plugin/server/src/index.ts.tmpl +59 -0
  232. package/dist/templates/plugin/server/tsconfig.json.tmpl +14 -0
  233. package/llms.txt +94 -0
  234. package/package.json +60 -0
@@ -0,0 +1,1188 @@
1
+ /**
2
+ * Config Editor — ts-morph AST manipulation of edgebase.config.ts
3
+ *
4
+ * Surgically edits the config file while preserving:
5
+ * - User comments
6
+ * - Formatting / indentation
7
+ * - rules, hooks, and other function expressions
8
+ * - Non-schema config (auth, cors, storage, etc.)
9
+ *
10
+ *
11
+ */
12
+ import { Project, SyntaxKind, IndentationText, NewLineKind, ts, } from 'ts-morph';
13
+ import { copyFileSync, readFileSync, writeFileSync, renameSync, mkdtempSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { tmpdir } from 'node:os';
16
+ // ─── Validation Constants ───
17
+ const VALID_FIELD_TYPES = ['string', 'text', 'number', 'boolean', 'datetime', 'json'];
18
+ const AUTO_FIELDS = ['id', 'createdAt', 'updatedAt'];
19
+ // ─── Internal Helpers ───
20
+ function gcd(a, b) {
21
+ return b === 0 ? a : gcd(b, a % b);
22
+ }
23
+ function detectIndentationText(text) {
24
+ const spaceIndentLengths = [];
25
+ let tabIndentedLines = 0;
26
+ for (const line of text.split(/\r?\n/)) {
27
+ if (!line.trim())
28
+ continue;
29
+ const match = line.match(/^([ \t]+)/);
30
+ if (!match)
31
+ continue;
32
+ const indent = match[1];
33
+ if (indent.includes('\t')) {
34
+ tabIndentedLines++;
35
+ continue;
36
+ }
37
+ spaceIndentLengths.push(indent.length);
38
+ }
39
+ if (tabIndentedLines > spaceIndentLengths.length) {
40
+ return {
41
+ indentationText: IndentationText.Tab,
42
+ indentSize: 4,
43
+ tabSize: 4,
44
+ convertTabsToSpaces: false,
45
+ };
46
+ }
47
+ if (spaceIndentLengths.length === 0) {
48
+ return {
49
+ indentationText: IndentationText.TwoSpaces,
50
+ indentSize: 2,
51
+ tabSize: 2,
52
+ convertTabsToSpaces: true,
53
+ };
54
+ }
55
+ const normalizedIndent = spaceIndentLengths.reduce((current, indent) => gcd(current, indent));
56
+ const indentSize = normalizedIndent >= 8 ? 8 : normalizedIndent >= 4 ? 4 : 2;
57
+ const indentationText = indentSize === 8
58
+ ? IndentationText.EightSpaces
59
+ : indentSize === 4
60
+ ? IndentationText.FourSpaces
61
+ : IndentationText.TwoSpaces;
62
+ return {
63
+ indentationText,
64
+ indentSize,
65
+ tabSize: indentSize,
66
+ convertTabsToSpaces: true,
67
+ };
68
+ }
69
+ function detectFormattingProfile(text) {
70
+ const newLineCharacter = text.includes('\r\n') ? '\r\n' : '\n';
71
+ const newLineKind = newLineCharacter === '\r\n'
72
+ ? NewLineKind.CarriageReturnLineFeed
73
+ : NewLineKind.LineFeed;
74
+ return {
75
+ ...detectIndentationText(text),
76
+ newLineKind,
77
+ newLineCharacter,
78
+ };
79
+ }
80
+ function createProject(formatting) {
81
+ return new Project({
82
+ manipulationSettings: {
83
+ indentationText: formatting.indentationText,
84
+ newLineKind: formatting.newLineKind,
85
+ usePrefixAndSuffixTextForRename: false,
86
+ useTrailingCommas: true,
87
+ },
88
+ compilerOptions: { allowJs: true },
89
+ useInMemoryFileSystem: false,
90
+ });
91
+ }
92
+ /**
93
+ * Get the ObjectLiteralExpression passed to defineConfig().
94
+ * Supports both:
95
+ * export default defineConfig({ ... })
96
+ * export default { ... }
97
+ */
98
+ function getConfigObject(sourceFile) {
99
+ // Strategy 1: find defineConfig(...) call
100
+ const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
101
+ for (const call of calls) {
102
+ const expr = call.getExpression();
103
+ if (expr.getText() === 'defineConfig') {
104
+ const args = call.getArguments();
105
+ if (args.length > 0 && args[0].isKind(SyntaxKind.ObjectLiteralExpression)) {
106
+ return args[0];
107
+ }
108
+ }
109
+ }
110
+ // Strategy 2: export default { ... }
111
+ const defaultExport = sourceFile.getDefaultExportSymbol();
112
+ if (defaultExport) {
113
+ const decl = defaultExport.getDeclarations()[0];
114
+ if (decl) {
115
+ const obj = decl.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression)[0];
116
+ if (obj)
117
+ return obj;
118
+ }
119
+ }
120
+ throw new Error('Cannot find config object in edgebase.config.ts. Expected defineConfig({ ... }) or export default { ... }');
121
+ }
122
+ /**
123
+ * Get or create the `databases` property, then navigate to `databases.{dbKey}.tables`.
124
+ * Creates missing intermediate structures.
125
+ */
126
+ function ensureTablesBlock(configObj, dbKey) {
127
+ // Get or create `databases`
128
+ let dbsProp = configObj.getProperty('databases');
129
+ if (!dbsProp) {
130
+ // Insert databases at beginning for prominence
131
+ configObj.insertPropertyAssignment(0, {
132
+ name: 'databases',
133
+ initializer: '{}',
134
+ });
135
+ dbsProp = configObj.getProperty('databases');
136
+ }
137
+ const dbsObj = dbsProp.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
138
+ // Get or create `databases.{dbKey}`
139
+ let blockProp = dbsObj.getProperty(dbKey);
140
+ if (!blockProp) {
141
+ dbsObj.addPropertyAssignment({
142
+ name: dbKey,
143
+ initializer: '{\n tables: {},\n }',
144
+ });
145
+ blockProp = dbsObj.getProperty(dbKey);
146
+ }
147
+ const blockObj = blockProp.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
148
+ // Get or create `databases.{dbKey}.tables`
149
+ let tablesProp = blockObj.getProperty('tables');
150
+ if (!tablesProp) {
151
+ blockObj.addPropertyAssignment({
152
+ name: 'tables',
153
+ initializer: '{}',
154
+ });
155
+ tablesProp = blockObj.getProperty('tables');
156
+ }
157
+ return tablesProp.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
158
+ }
159
+ /**
160
+ * Navigate to an existing table's config object.
161
+ * Returns [tablesBlock, tableBlock, dbKey] or throws.
162
+ */
163
+ function findTable(configObj, tableName, expectedDbKey) {
164
+ const dbsProp = configObj.getProperty('databases');
165
+ if (!dbsProp)
166
+ throw new Error(`No databases block found in config.`);
167
+ const dbsObj = dbsProp.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
168
+ for (const prop of dbsObj.getProperties()) {
169
+ if (!prop.isKind(SyntaxKind.PropertyAssignment))
170
+ continue;
171
+ const dbKey = prop.getName().replace(/['"]/g, '');
172
+ if (expectedDbKey && dbKey !== expectedDbKey)
173
+ continue;
174
+ const blockObj = prop.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
175
+ const tablesProp = blockObj.getProperty('tables');
176
+ if (!tablesProp)
177
+ continue;
178
+ const tablesObj = tablesProp.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
179
+ const tableProp = tablesObj.getProperty(tableName);
180
+ if (tableProp) {
181
+ return {
182
+ tablesBlock: tablesObj,
183
+ tableBlock: tableProp.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression),
184
+ dbKey,
185
+ };
186
+ }
187
+ }
188
+ const scope = expectedDbKey ? `databases.${expectedDbKey}` : 'any database block';
189
+ throw new Error(`Table '${tableName}' not found in ${scope}.`);
190
+ }
191
+ /**
192
+ * Get or create the `schema` property inside a table config object.
193
+ */
194
+ function ensureSchemaBlock(tableBlock) {
195
+ let schemaProp = tableBlock.getProperty('schema');
196
+ if (!schemaProp) {
197
+ tableBlock.insertPropertyAssignment(0, {
198
+ name: 'schema',
199
+ initializer: '{}',
200
+ });
201
+ schemaProp = tableBlock.getProperty('schema');
202
+ }
203
+ return schemaProp.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
204
+ }
205
+ /**
206
+ * Collect all table names across all DB blocks for uniqueness check.
207
+ */
208
+ function getAllTableNames(configObj) {
209
+ const map = new Map(); // tableName → dbKey
210
+ const dbsProp = configObj.getProperty('databases');
211
+ if (!dbsProp)
212
+ return map;
213
+ try {
214
+ const dbsObj = dbsProp.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
215
+ for (const prop of dbsObj.getProperties()) {
216
+ if (!prop.isKind(SyntaxKind.PropertyAssignment))
217
+ continue;
218
+ const dbKey = prop.getName().replace(/['"]/g, '');
219
+ try {
220
+ const blockObj = prop.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
221
+ const tablesProp = blockObj.getProperty('tables');
222
+ if (!tablesProp)
223
+ continue;
224
+ const tablesObj = tablesProp.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
225
+ for (const tProp of tablesObj.getProperties()) {
226
+ if (!tProp.isKind(SyntaxKind.PropertyAssignment))
227
+ continue;
228
+ const tName = tProp.getName().replace(/['"]/g, '');
229
+ map.set(tName, dbKey);
230
+ }
231
+ }
232
+ catch {
233
+ // Skip non-object blocks
234
+ }
235
+ }
236
+ }
237
+ catch {
238
+ // No databases block
239
+ }
240
+ return map;
241
+ }
242
+ // ─── Serialization ───
243
+ function serializeSchemaField(field) {
244
+ const parts = [];
245
+ parts.push(`type: '${field.type}'`);
246
+ if (field.required !== undefined)
247
+ parts.push(`required: ${field.required}`);
248
+ if (field.unique !== undefined)
249
+ parts.push(`unique: ${field.unique}`);
250
+ if (field.default !== undefined)
251
+ parts.push(`default: ${JSON.stringify(field.default)}`);
252
+ if (field.primaryKey !== undefined)
253
+ parts.push(`primaryKey: ${field.primaryKey}`);
254
+ if (field.min !== undefined)
255
+ parts.push(`min: ${field.min}`);
256
+ if (field.max !== undefined)
257
+ parts.push(`max: ${field.max}`);
258
+ if (field.pattern !== undefined)
259
+ parts.push(`pattern: '${field.pattern}'`);
260
+ if (field.enum !== undefined && field.enum.length > 0) {
261
+ parts.push(`enum: [${field.enum.map(e => `'${e}'`).join(', ')}]`);
262
+ }
263
+ if (field.onUpdate !== undefined)
264
+ parts.push(`onUpdate: '${field.onUpdate}'`);
265
+ if (field.references !== undefined) {
266
+ if (typeof field.references === 'string') {
267
+ parts.push(`references: '${field.references}'`);
268
+ }
269
+ else {
270
+ const ref = field.references;
271
+ const refParts = [`table: '${ref.table}'`];
272
+ if (ref.column)
273
+ refParts.push(`column: '${ref.column}'`);
274
+ if (ref.onDelete)
275
+ refParts.push(`onDelete: '${ref.onDelete}'`);
276
+ if (ref.onUpdate)
277
+ refParts.push(`onUpdate: '${ref.onUpdate}'`);
278
+ parts.push(`references: { ${refParts.join(', ')} }`);
279
+ }
280
+ }
281
+ if (field.check !== undefined)
282
+ parts.push(`check: '${field.check}'`);
283
+ return `{ ${parts.join(', ')} }`;
284
+ }
285
+ function serializeSchema(schema) {
286
+ if (Object.keys(schema).length === 0)
287
+ return '{}';
288
+ const lines = [];
289
+ for (const [name, field] of Object.entries(schema)) {
290
+ lines.push(`${name}: ${serializeSchemaField(field)},`);
291
+ }
292
+ return `{\n ${lines.join('\n ')}\n }`;
293
+ }
294
+ function serializeIndexConfig(idx) {
295
+ const parts = [];
296
+ parts.push(`fields: [${idx.fields.map(f => `'${f}'`).join(', ')}]`);
297
+ if (idx.unique)
298
+ parts.push(`unique: true`);
299
+ return `{ ${parts.join(', ')} }`;
300
+ }
301
+ // ─── File I/O ───
302
+ function backupConfig(configPath) {
303
+ copyFileSync(configPath, `${configPath}.bak`);
304
+ }
305
+ function atomicSave(sourceFile, configPath, formatting) {
306
+ sourceFile.formatText({
307
+ indentSize: formatting.indentSize,
308
+ tabSize: formatting.tabSize,
309
+ convertTabsToSpaces: formatting.convertTabsToSpaces,
310
+ newLineCharacter: formatting.newLineCharacter,
311
+ indentStyle: ts.IndentStyle.Smart,
312
+ });
313
+ const tempDir = mkdtempSync(join(tmpdir(), 'edgebase-config-'));
314
+ const tempPath = join(tempDir, 'edgebase.config.ts');
315
+ writeFileSync(tempPath, sourceFile.getFullText(), 'utf-8');
316
+ renameSync(tempPath, configPath);
317
+ }
318
+ function loadAndParse(opts) {
319
+ const formatting = detectFormattingProfile(readFileSync(opts.configPath, 'utf-8'));
320
+ const project = createProject(formatting);
321
+ const sourceFile = project.addSourceFileAtPath(opts.configPath);
322
+ const configObj = getConfigObject(sourceFile);
323
+ return { sourceFile, configObj, formatting };
324
+ }
325
+ function saveWithBackup(opts, sourceFile, formatting) {
326
+ if (opts.backup !== false) {
327
+ backupConfig(opts.configPath);
328
+ }
329
+ atomicSave(sourceFile, opts.configPath, formatting);
330
+ }
331
+ // ─── Validation ───
332
+ function validateFieldType(type) {
333
+ if (!VALID_FIELD_TYPES.includes(type)) {
334
+ throw new Error(`Invalid field type '${type}'. Must be one of: ${VALID_FIELD_TYPES.join(', ')}`);
335
+ }
336
+ }
337
+ function validateColumnName(columnName) {
338
+ if (AUTO_FIELDS.includes(columnName)) {
339
+ throw new Error(`Cannot add/modify auto-field '${columnName}'. Auto-fields (id, createdAt, updatedAt) are managed by the system.`);
340
+ }
341
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName)) {
342
+ throw new Error(`Invalid column name '${columnName}'. Must start with a letter or underscore, followed by alphanumeric or underscore.`);
343
+ }
344
+ }
345
+ function validateTableName(name) {
346
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
347
+ throw new Error(`Invalid table name '${name}'. Must start with a letter or underscore, followed by alphanumeric or underscore.`);
348
+ }
349
+ }
350
+ function validateDbKey(name) {
351
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
352
+ throw new Error(`Invalid database block name '${name}'. Must start with a letter or underscore, followed by alphanumeric or underscore.`);
353
+ }
354
+ }
355
+ function validateBucketName(name) {
356
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
357
+ throw new Error(`Invalid bucket name '${name}'. Use letters, numbers, dashes, and underscores, starting with a letter.`);
358
+ }
359
+ }
360
+ function toObjectPropertyName(name) {
361
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
362
+ }
363
+ function serializeDatabaseBlock(options) {
364
+ const lines = ['{'];
365
+ if (options.topology === 'dynamic') {
366
+ lines.push(` instance: true,`);
367
+ if (options.targetLabel || options.placeholder || options.helperText) {
368
+ lines.push(` admin: {`);
369
+ lines.push(` instances: {`);
370
+ lines.push(` source: 'manual',`);
371
+ if (options.targetLabel) {
372
+ lines.push(` targetLabel: '${options.targetLabel.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}',`);
373
+ }
374
+ if (options.placeholder) {
375
+ lines.push(` placeholder: '${options.placeholder.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}',`);
376
+ }
377
+ if (options.helperText) {
378
+ lines.push(` helperText: '${options.helperText.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}',`);
379
+ }
380
+ lines.push(` },`);
381
+ lines.push(` },`);
382
+ }
383
+ }
384
+ else if (options.provider && options.provider !== 'd1') {
385
+ lines.push(` provider: '${options.provider}',`);
386
+ }
387
+ if (options.topology === 'single' && (options.provider === 'neon' || options.provider === 'postgres')) {
388
+ lines.push(` connectionString: '${(options.connectionString ?? '').replace(/\\/g, '\\\\').replace(/'/g, "\\'")}',`);
389
+ }
390
+ lines.push(` tables: {},`);
391
+ lines.push(` }`);
392
+ return lines.join('\n');
393
+ }
394
+ // ─── Public API ───
395
+ /**
396
+ * Add a new database block to the config.
397
+ */
398
+ export async function addDatabaseBlock(opts, dbKey, options) {
399
+ validateDbKey(dbKey);
400
+ if (options.topology === 'dynamic' && options.provider && options.provider !== 'do') {
401
+ throw new Error(`Dynamic database blocks must use provider 'do'.`);
402
+ }
403
+ if (options.topology === 'single' && (options.provider === 'neon' || options.provider === 'postgres') && !options.connectionString) {
404
+ throw new Error(`connectionString is required when provider is '${options.provider}'.`);
405
+ }
406
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
407
+ const databasesBlock = ensureNestedBlock(configObj, ['databases']);
408
+ const existing = databasesBlock.getProperty(dbKey);
409
+ if (existing) {
410
+ throw new Error(`Database block '${dbKey}' already exists.`);
411
+ }
412
+ databasesBlock.addPropertyAssignment({
413
+ name: dbKey,
414
+ initializer: serializeDatabaseBlock(options),
415
+ });
416
+ saveWithBackup(opts, sourceFile, formatting);
417
+ }
418
+ /**
419
+ * Update a database block's provider settings.
420
+ * Used by dashboard upgrade/setup flows that rewrite a namespace from D1/DO to PostgreSQL.
421
+ */
422
+ export async function updateDatabaseBlock(opts, dbKey, options) {
423
+ validateDbKey(dbKey);
424
+ if (options.provider === 'neon' || options.provider === 'postgres') {
425
+ if (!options.connectionString) {
426
+ throw new Error(`connectionString is required when provider is '${options.provider}'.`);
427
+ }
428
+ }
429
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
430
+ const databasesBlock = ensureNestedBlock(configObj, ['databases']);
431
+ const dbProp = databasesBlock.getProperty(dbKey);
432
+ if (!dbProp) {
433
+ throw new Error(`Database block '${dbKey}' not found.`);
434
+ }
435
+ const dbBlock = dbProp.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
436
+ const currentInstance = dbBlock.getProperty('instance');
437
+ if (currentInstance && options.provider && options.provider !== 'do') {
438
+ throw new Error(`Dynamic database blocks must use provider 'do'.`);
439
+ }
440
+ const providerProp = dbBlock.getProperty('provider');
441
+ const connectionStringProp = dbBlock.getProperty('connectionString');
442
+ const nextProvider = options.provider;
443
+ if (!nextProvider || nextProvider === 'd1') {
444
+ providerProp?.remove();
445
+ }
446
+ else if (providerProp) {
447
+ providerProp.setInitializer(`'${nextProvider}'`);
448
+ }
449
+ else {
450
+ dbBlock.insertPropertyAssignment(0, {
451
+ name: 'provider',
452
+ initializer: `'${nextProvider}'`,
453
+ });
454
+ }
455
+ const nextConnectionString = options.connectionString ?? undefined;
456
+ if (nextProvider === 'neon' || nextProvider === 'postgres') {
457
+ const serialized = `'${nextConnectionString.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
458
+ if (connectionStringProp) {
459
+ connectionStringProp.setInitializer(serialized);
460
+ }
461
+ else {
462
+ const insertIndex = dbBlock.getProperties().findIndex((prop) => prop.getKind() === SyntaxKind.PropertyAssignment && prop.getName() === 'tables');
463
+ dbBlock.insertPropertyAssignment(insertIndex >= 0 ? insertIndex : dbBlock.getProperties().length, {
464
+ name: 'connectionString',
465
+ initializer: serialized,
466
+ });
467
+ }
468
+ }
469
+ else {
470
+ connectionStringProp?.remove();
471
+ }
472
+ saveWithBackup(opts, sourceFile, formatting);
473
+ }
474
+ /**
475
+ * Add a new storage bucket to the config.
476
+ */
477
+ export async function addStorageBucket(opts, bucketName) {
478
+ validateBucketName(bucketName);
479
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
480
+ const bucketsBlock = ensureNestedBlock(configObj, ['storage', 'buckets']);
481
+ const existing = bucketsBlock.getProperty(bucketName);
482
+ if (existing) {
483
+ throw new Error(`Storage bucket '${bucketName}' already exists.`);
484
+ }
485
+ bucketsBlock.addPropertyAssignment({
486
+ name: toObjectPropertyName(bucketName),
487
+ initializer: '{}',
488
+ });
489
+ saveWithBackup(opts, sourceFile, formatting);
490
+ }
491
+ /**
492
+ * Add a new table to the config.
493
+ */
494
+ export async function addTable(opts, dbKey, tableName, schema) {
495
+ validateTableName(tableName);
496
+ for (const [colName, field] of Object.entries(schema)) {
497
+ validateColumnName(colName);
498
+ validateFieldType(field.type);
499
+ }
500
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
501
+ // Check uniqueness across all DB blocks
502
+ const existing = getAllTableNames(configObj);
503
+ if (existing.has(tableName)) {
504
+ throw new Error(`Table '${tableName}' already exists in database block '${existing.get(tableName)}'.`);
505
+ }
506
+ const tablesBlock = ensureTablesBlock(configObj, dbKey);
507
+ const schemaStr = serializeSchema(schema);
508
+ tablesBlock.addPropertyAssignment({
509
+ name: tableName,
510
+ initializer: `{\n schema: ${schemaStr},\n }`,
511
+ });
512
+ saveWithBackup(opts, sourceFile, formatting);
513
+ }
514
+ /**
515
+ * Remove a table from the config.
516
+ */
517
+ export async function removeTable(opts, dbKey, tableName) {
518
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
519
+ const { tablesBlock } = findTable(configObj, tableName, dbKey);
520
+ const prop = tablesBlock.getProperty(tableName);
521
+ if (prop)
522
+ prop.remove();
523
+ saveWithBackup(opts, sourceFile, formatting);
524
+ }
525
+ /**
526
+ * Rename a table.
527
+ */
528
+ export async function renameTable(opts, dbKey, oldName, newName) {
529
+ validateTableName(newName);
530
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
531
+ // Check new name doesn't exist
532
+ const existing = getAllTableNames(configObj);
533
+ if (existing.has(newName)) {
534
+ throw new Error(`Table '${newName}' already exists in database block '${existing.get(newName)}'.`);
535
+ }
536
+ const { tablesBlock } = findTable(configObj, oldName, dbKey);
537
+ const prop = tablesBlock.getProperty(oldName);
538
+ if (!prop)
539
+ throw new Error(`Table '${oldName}' not found.`);
540
+ // Rename the property
541
+ prop.rename(newName);
542
+ saveWithBackup(opts, sourceFile, formatting);
543
+ }
544
+ /**
545
+ * Add a column to a table's schema.
546
+ */
547
+ export async function addColumn(opts, dbKey, tableName, columnName, fieldDef) {
548
+ validateColumnName(columnName);
549
+ validateFieldType(fieldDef.type);
550
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
551
+ const { tableBlock } = findTable(configObj, tableName, dbKey);
552
+ const schemaBlock = ensureSchemaBlock(tableBlock);
553
+ // Check column doesn't already exist
554
+ if (schemaBlock.getProperty(columnName)) {
555
+ throw new Error(`Column '${columnName}' already exists in table '${tableName}'.`);
556
+ }
557
+ schemaBlock.addPropertyAssignment({
558
+ name: columnName,
559
+ initializer: serializeSchemaField(fieldDef),
560
+ });
561
+ saveWithBackup(opts, sourceFile, formatting);
562
+ }
563
+ /**
564
+ * Remove a column from a table's schema.
565
+ */
566
+ export async function removeColumn(opts, dbKey, tableName, columnName) {
567
+ validateColumnName(columnName);
568
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
569
+ const { tableBlock } = findTable(configObj, tableName, dbKey);
570
+ const schemaBlock = ensureSchemaBlock(tableBlock);
571
+ const prop = schemaBlock.getProperty(columnName);
572
+ if (!prop) {
573
+ throw new Error(`Column '${columnName}' not found in table '${tableName}'.`);
574
+ }
575
+ prop.remove();
576
+ saveWithBackup(opts, sourceFile, formatting);
577
+ }
578
+ /**
579
+ * Update a column's field definition.
580
+ * Partial update: only specified keys are changed.
581
+ */
582
+ export async function updateColumn(opts, dbKey, tableName, columnName, fieldDef) {
583
+ if (AUTO_FIELDS.includes(columnName)) {
584
+ throw new Error(`Cannot modify auto-field '${columnName}'.`);
585
+ }
586
+ if (fieldDef.type)
587
+ validateFieldType(fieldDef.type);
588
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
589
+ const { tableBlock } = findTable(configObj, tableName, dbKey);
590
+ const schemaBlock = ensureSchemaBlock(tableBlock);
591
+ const prop = schemaBlock.getProperty(columnName);
592
+ if (!prop) {
593
+ throw new Error(`Column '${columnName}' not found in table '${tableName}'.`);
594
+ }
595
+ // Read current field, merge with update, rewrite
596
+ const currentObj = prop.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
597
+ const currentField = {};
598
+ for (const p of currentObj.getProperties()) {
599
+ if (!p.isKind(SyntaxKind.PropertyAssignment))
600
+ continue;
601
+ const name = p.getName().replace(/['"]/g, '');
602
+ const initText = p.getInitializer()?.getText() ?? '';
603
+ // Parse basic values
604
+ if (initText === 'true')
605
+ currentField[name] = true;
606
+ else if (initText === 'false')
607
+ currentField[name] = false;
608
+ else if (/^\d+(\.\d+)?$/.test(initText))
609
+ currentField[name] = parseFloat(initText);
610
+ else if (initText.startsWith("'") || initText.startsWith('"'))
611
+ currentField[name] = initText.slice(1, -1);
612
+ else
613
+ currentField[name] = initText; // Keep complex expressions as-is
614
+ }
615
+ // Merge
616
+ const merged = { ...currentField, ...fieldDef };
617
+ // Remove undefined keys
618
+ for (const [k, v] of Object.entries(merged)) {
619
+ if (v === undefined)
620
+ delete merged[k];
621
+ }
622
+ // Replace the entire initializer
623
+ prop.setInitializer(serializeSchemaField(merged));
624
+ saveWithBackup(opts, sourceFile, formatting);
625
+ }
626
+ /**
627
+ * Add an index to a table.
628
+ */
629
+ export async function addIndex(opts, dbKey, tableName, indexDef) {
630
+ if (!indexDef.fields || indexDef.fields.length === 0) {
631
+ throw new Error('Index must have at least one field.');
632
+ }
633
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
634
+ const { tableBlock } = findTable(configObj, tableName, dbKey);
635
+ let indexesProp = tableBlock.getProperty('indexes');
636
+ if (!indexesProp) {
637
+ tableBlock.addPropertyAssignment({
638
+ name: 'indexes',
639
+ initializer: '[]',
640
+ });
641
+ indexesProp = tableBlock.getProperty('indexes');
642
+ }
643
+ const arr = indexesProp.getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression);
644
+ arr.addElement(serializeIndexConfig(indexDef));
645
+ saveWithBackup(opts, sourceFile, formatting);
646
+ }
647
+ /**
648
+ * Remove an index from a table by its position (0-based).
649
+ */
650
+ export async function removeIndex(opts, dbKey, tableName, indexIdx) {
651
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
652
+ const { tableBlock } = findTable(configObj, tableName, dbKey);
653
+ const indexesProp = tableBlock.getProperty('indexes');
654
+ if (!indexesProp)
655
+ throw new Error(`Table '${tableName}' has no indexes.`);
656
+ const arr = indexesProp.getInitializerIfKindOrThrow(SyntaxKind.ArrayLiteralExpression);
657
+ const elements = arr.getElements();
658
+ if (indexIdx < 0 || indexIdx >= elements.length) {
659
+ throw new Error(`Index position ${indexIdx} out of range (0-${elements.length - 1}).`);
660
+ }
661
+ arr.removeElement(indexIdx);
662
+ saveWithBackup(opts, sourceFile, formatting);
663
+ }
664
+ /**
665
+ * Set FTS (Full-Text Search) fields for a table.
666
+ * Pass empty array to remove FTS.
667
+ */
668
+ export async function setFts(opts, dbKey, tableName, fields) {
669
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
670
+ const { tableBlock } = findTable(configObj, tableName, dbKey);
671
+ const ftsProp = tableBlock.getProperty('fts');
672
+ if (fields.length === 0) {
673
+ // Remove FTS
674
+ if (ftsProp)
675
+ ftsProp.remove();
676
+ }
677
+ else {
678
+ const ftsStr = `[${fields.map(f => `'${f}'`).join(', ')}]`;
679
+ if (ftsProp) {
680
+ ftsProp.setInitializer(ftsStr);
681
+ }
682
+ else {
683
+ tableBlock.addPropertyAssignment({
684
+ name: 'fts',
685
+ initializer: ftsStr,
686
+ });
687
+ }
688
+ }
689
+ saveWithBackup(opts, sourceFile, formatting);
690
+ }
691
+ function quoteString(value) {
692
+ return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
693
+ }
694
+ function quoteStringArray(values) {
695
+ return `[${values.map((value) => quoteString(value)).join(', ')}]`;
696
+ }
697
+ function upsertProperty(parent, key, initializer) {
698
+ const existing = parent.getProperty(key);
699
+ if (existing) {
700
+ existing.setInitializer(initializer);
701
+ return;
702
+ }
703
+ parent.addPropertyAssignment({
704
+ name: toObjectPropertyName(key),
705
+ initializer,
706
+ });
707
+ }
708
+ function removeProperty(parent, key) {
709
+ const existing = parent.getProperty(key);
710
+ existing?.remove();
711
+ }
712
+ function setBooleanProperty(parent, key, value) {
713
+ if (value === undefined)
714
+ return;
715
+ upsertProperty(parent, key, value ? 'true' : 'false');
716
+ }
717
+ function setStringProperty(parent, key, value) {
718
+ if (value === undefined)
719
+ return;
720
+ if (value === null || value.trim() === '') {
721
+ removeProperty(parent, key);
722
+ return;
723
+ }
724
+ upsertProperty(parent, key, quoteString(value.trim()));
725
+ }
726
+ function setNumberProperty(parent, key, value) {
727
+ if (value === undefined)
728
+ return;
729
+ if (value === null || Number.isNaN(value)) {
730
+ removeProperty(parent, key);
731
+ return;
732
+ }
733
+ upsertProperty(parent, key, String(value));
734
+ }
735
+ function setStringArrayProperty(parent, key, values, options) {
736
+ if (values === undefined)
737
+ return;
738
+ const normalized = values
739
+ .map((value) => value.trim())
740
+ .filter(Boolean);
741
+ if (normalized.length === 0 && options?.removeWhenEmpty) {
742
+ removeProperty(parent, key);
743
+ return;
744
+ }
745
+ upsertProperty(parent, key, quoteStringArray(normalized));
746
+ }
747
+ function setExpressionProperty(parent, key, initializer) {
748
+ if (initializer === undefined)
749
+ return;
750
+ if (initializer === null || initializer.trim() === '') {
751
+ removeProperty(parent, key);
752
+ return;
753
+ }
754
+ upsertProperty(parent, key, initializer);
755
+ }
756
+ function pruneEmptyNestedBlock(configObj, path) {
757
+ if (path.length === 0)
758
+ return;
759
+ const block = getNestedBlock(configObj, path);
760
+ if (!block || block.getProperties().length > 0)
761
+ return;
762
+ const parentPath = path.slice(0, -1);
763
+ const key = path[path.length - 1];
764
+ removeNestedProperty(configObj, parentPath, key);
765
+ }
766
+ function normalizeEnvSegment(value) {
767
+ return value
768
+ .trim()
769
+ .replace(/[^A-Za-z0-9]+/g, '_')
770
+ .replace(/^_+|_+$/g, '')
771
+ .toUpperCase();
772
+ }
773
+ function getOAuthEnvKey(provider, field) {
774
+ if (provider.startsWith('oidc:')) {
775
+ const oidcName = normalizeEnvSegment(provider.slice(5)) || 'CUSTOM';
776
+ return `EDGEBASE_OIDC_${oidcName}_${field}`;
777
+ }
778
+ const providerName = normalizeEnvSegment(provider) || 'CUSTOM';
779
+ return `EDGEBASE_OAUTH_${providerName}_${field}`;
780
+ }
781
+ function getAllowedOAuthProvidersExpression() {
782
+ return `Array.from(new Set((process.env.EDGEBASE_AUTH_ALLOWED_OAUTH_PROVIDERS ?? '').split(',').map((entry) => entry.trim()).filter(Boolean)))`;
783
+ }
784
+ function hasOAuthValue(config) {
785
+ return ((typeof config.clientId === 'string' && config.clientId.trim().length > 0)
786
+ || (typeof config.clientSecret === 'string' && config.clientSecret.trim().length > 0)
787
+ || (typeof config.issuer === 'string' && config.issuer.trim().length > 0)
788
+ || (Array.isArray(config.scopes) && config.scopes.some((scope) => scope.trim().length > 0)));
789
+ }
790
+ function hasSpreadBindingForKey(parent, key) {
791
+ return parent
792
+ .getProperties()
793
+ .some((prop) => prop.getKind() === SyntaxKind.SpreadAssignment && new RegExp(`\\b${key}\\s*:`).test(prop.getText()));
794
+ }
795
+ function getManagedOAuthBlock(configObj, authBlock) {
796
+ const oauthProperty = authBlock.getProperty('oauth');
797
+ if (oauthProperty?.isKind(SyntaxKind.PropertyAssignment)) {
798
+ const initializer = oauthProperty.getInitializer();
799
+ if (initializer?.isKind(SyntaxKind.ObjectLiteralExpression)) {
800
+ return initializer;
801
+ }
802
+ return null;
803
+ }
804
+ if (hasSpreadBindingForKey(authBlock, 'oauth')) {
805
+ return null;
806
+ }
807
+ return ensureNestedBlock(configObj, ['auth', 'oauth']);
808
+ }
809
+ function setAllowedOAuthProvidersBinding(authBlock, allowedOAuthProviders) {
810
+ if (allowedOAuthProviders === undefined)
811
+ return;
812
+ if (hasSpreadBindingForKey(authBlock, 'allowedOAuthProviders'))
813
+ return;
814
+ setExpressionProperty(authBlock, 'allowedOAuthProviders', getAllowedOAuthProvidersExpression());
815
+ }
816
+ function configureOAuthProviderBlock(providerBlock, provider, config) {
817
+ if (!provider.startsWith('oidc:')) {
818
+ if (!hasOAuthValue(config)) {
819
+ removeProperty(providerBlock, 'clientId');
820
+ removeProperty(providerBlock, 'clientSecret');
821
+ removeProperty(providerBlock, 'issuer');
822
+ removeProperty(providerBlock, 'scopes');
823
+ return;
824
+ }
825
+ setExpressionProperty(providerBlock, 'clientId', `process.env.${getOAuthEnvKey(provider, 'CLIENT_ID')} ?? ''`);
826
+ setExpressionProperty(providerBlock, 'clientSecret', `process.env.${getOAuthEnvKey(provider, 'CLIENT_SECRET')} ?? ''`);
827
+ removeProperty(providerBlock, 'issuer');
828
+ removeProperty(providerBlock, 'scopes');
829
+ return;
830
+ }
831
+ setExpressionProperty(providerBlock, 'clientId', hasOAuthValue(config) ? `process.env.${getOAuthEnvKey(provider, 'CLIENT_ID')} ?? ''` : null);
832
+ setExpressionProperty(providerBlock, 'clientSecret', hasOAuthValue(config) ? `process.env.${getOAuthEnvKey(provider, 'CLIENT_SECRET')} ?? ''` : null);
833
+ setStringProperty(providerBlock, 'issuer', config.issuer);
834
+ setStringArrayProperty(providerBlock, 'scopes', config.scopes, { removeWhenEmpty: true });
835
+ }
836
+ export async function setAuthSettings(opts, settings) {
837
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
838
+ const authBlock = ensureNestedBlock(configObj, ['auth']);
839
+ setBooleanProperty(authBlock, 'emailAuth', settings.emailAuth);
840
+ setBooleanProperty(authBlock, 'anonymousAuth', settings.anonymousAuth);
841
+ setAllowedOAuthProvidersBinding(authBlock, settings.allowedOAuthProviders);
842
+ setStringArrayProperty(authBlock, 'allowedRedirectUrls', settings.allowedRedirectUrls, {
843
+ removeWhenEmpty: true,
844
+ });
845
+ if (settings.session) {
846
+ const sessionBlock = ensureNestedBlock(configObj, ['auth', 'session']);
847
+ setStringProperty(sessionBlock, 'accessTokenTTL', settings.session.accessTokenTTL);
848
+ setStringProperty(sessionBlock, 'refreshTokenTTL', settings.session.refreshTokenTTL);
849
+ setNumberProperty(sessionBlock, 'maxActiveSessions', settings.session.maxActiveSessions);
850
+ pruneEmptyNestedBlock(configObj, ['auth', 'session']);
851
+ }
852
+ if (settings.magicLink) {
853
+ const magicLinkBlock = ensureNestedBlock(configObj, ['auth', 'magicLink']);
854
+ setBooleanProperty(magicLinkBlock, 'enabled', settings.magicLink.enabled);
855
+ setBooleanProperty(magicLinkBlock, 'autoCreate', settings.magicLink.autoCreate);
856
+ setStringProperty(magicLinkBlock, 'tokenTTL', settings.magicLink.tokenTTL);
857
+ pruneEmptyNestedBlock(configObj, ['auth', 'magicLink']);
858
+ }
859
+ if (settings.emailOtp) {
860
+ const emailOtpBlock = ensureNestedBlock(configObj, ['auth', 'emailOtp']);
861
+ setBooleanProperty(emailOtpBlock, 'enabled', settings.emailOtp.enabled);
862
+ setBooleanProperty(emailOtpBlock, 'autoCreate', settings.emailOtp.autoCreate);
863
+ pruneEmptyNestedBlock(configObj, ['auth', 'emailOtp']);
864
+ }
865
+ if (settings.passkeys) {
866
+ const passkeysBlock = ensureNestedBlock(configObj, ['auth', 'passkeys']);
867
+ setBooleanProperty(passkeysBlock, 'enabled', settings.passkeys.enabled);
868
+ setStringProperty(passkeysBlock, 'rpName', settings.passkeys.rpName);
869
+ setStringProperty(passkeysBlock, 'rpID', settings.passkeys.rpID);
870
+ setStringArrayProperty(passkeysBlock, 'origin', settings.passkeys.origin, { removeWhenEmpty: true });
871
+ pruneEmptyNestedBlock(configObj, ['auth', 'passkeys']);
872
+ }
873
+ if (settings.oauth) {
874
+ const oauthBlock = getManagedOAuthBlock(configObj, authBlock);
875
+ for (const [provider, providerConfig] of Object.entries(settings.oauth)) {
876
+ if (!oauthBlock && !provider.startsWith('oidc:')) {
877
+ continue;
878
+ }
879
+ if (provider.startsWith('oidc:')) {
880
+ const oidcName = provider.slice(5);
881
+ if (!oauthBlock) {
882
+ continue;
883
+ }
884
+ const oidcBlock = ensureNestedBlock(configObj, ['auth', 'oauth', 'oidc']);
885
+ const providerBlock = ensureNestedBlock(oidcBlock, [oidcName]);
886
+ configureOAuthProviderBlock(providerBlock, provider, providerConfig);
887
+ if (providerBlock.getProperties().length === 0) {
888
+ removeProperty(oidcBlock, oidcName);
889
+ }
890
+ continue;
891
+ }
892
+ if (!oauthBlock)
893
+ continue;
894
+ const providerBlock = ensureNestedBlock(oauthBlock, [provider]);
895
+ configureOAuthProviderBlock(providerBlock, provider, providerConfig);
896
+ if (providerBlock.getProperties().length === 0) {
897
+ removeProperty(oauthBlock, provider);
898
+ }
899
+ }
900
+ if (oauthBlock) {
901
+ pruneEmptyNestedBlock(configObj, ['auth', 'oauth', 'oidc']);
902
+ pruneEmptyNestedBlock(configObj, ['auth', 'oauth']);
903
+ }
904
+ }
905
+ pruneEmptyNestedBlock(configObj, ['auth']);
906
+ saveWithBackup(opts, sourceFile, formatting);
907
+ }
908
+ // ─── Email Config Editing ───
909
+ const VALID_EMAIL_TYPES = ['verification', 'passwordReset', 'magicLink', 'emailOtp', 'emailChange'];
910
+ function validateEmailType(type) {
911
+ if (!VALID_EMAIL_TYPES.includes(type)) {
912
+ throw new Error(`Invalid email type '${type}'. Must be one of: ${VALID_EMAIL_TYPES.join(', ')}`);
913
+ }
914
+ }
915
+ /**
916
+ * Ensure a nested property path exists on config object.
917
+ * e.g., ensureNestedBlock(configObj, ['email', 'subjects']) → returns the `subjects` ObjectLiteralExpression.
918
+ */
919
+ function ensureNestedBlock(configObj, path) {
920
+ let current = configObj;
921
+ for (const key of path) {
922
+ let prop = current.getProperty(key);
923
+ if (!prop) {
924
+ current.addPropertyAssignment({
925
+ name: key,
926
+ initializer: '{}',
927
+ });
928
+ prop = current.getProperty(key);
929
+ }
930
+ current = prop.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
931
+ }
932
+ return current;
933
+ }
934
+ /**
935
+ * Remove a property from a nested path, cleaning up empty parent objects.
936
+ * e.g., removeNestedProperty(configObj, ['email', 'subjects'], 'verification')
937
+ */
938
+ function removeNestedProperty(configObj, parentPath, key) {
939
+ // Navigate to parent
940
+ let current = configObj;
941
+ const ancestors = [];
942
+ for (const segment of parentPath) {
943
+ const prop = current.getProperty(segment);
944
+ if (!prop)
945
+ return; // Path doesn't exist, nothing to remove
946
+ ancestors.push({ obj: current, key: segment });
947
+ try {
948
+ current = prop.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
949
+ }
950
+ catch {
951
+ return; // Not an object, nothing to remove
952
+ }
953
+ }
954
+ // Remove the target property
955
+ const targetProp = current.getProperty(key);
956
+ if (!targetProp)
957
+ return;
958
+ targetProp.remove();
959
+ // Clean up empty parent objects (bottom-up)
960
+ for (let i = ancestors.length - 1; i >= 0; i--) {
961
+ const { obj, key: parentKey } = ancestors[i];
962
+ const parentProp = obj.getProperty(parentKey);
963
+ if (!parentProp)
964
+ break;
965
+ try {
966
+ const parentObj = parentProp.getInitializerIfKindOrThrow(SyntaxKind.ObjectLiteralExpression);
967
+ if (parentObj.getProperties().length === 0) {
968
+ parentProp.remove();
969
+ }
970
+ else {
971
+ break; // Parent still has properties, stop cleanup
972
+ }
973
+ }
974
+ catch {
975
+ break;
976
+ }
977
+ }
978
+ }
979
+ /**
980
+ * Set a custom email subject override in config.
981
+ * Sets `email.subjects[type] = value`.
982
+ * If value is empty string, removes the override instead.
983
+ */
984
+ export async function setEmailSubject(opts, type, value) {
985
+ validateEmailType(type);
986
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
987
+ if (!value) {
988
+ // Remove the override
989
+ removeNestedProperty(configObj, ['email', 'subjects'], type);
990
+ }
991
+ else {
992
+ const subjectsBlock = ensureNestedBlock(configObj, ['email', 'subjects']);
993
+ const existing = subjectsBlock.getProperty(type);
994
+ const escaped = value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
995
+ if (existing) {
996
+ existing.setInitializer(`'${escaped}'`);
997
+ }
998
+ else {
999
+ subjectsBlock.addPropertyAssignment({
1000
+ name: type,
1001
+ initializer: `'${escaped}'`,
1002
+ });
1003
+ }
1004
+ }
1005
+ saveWithBackup(opts, sourceFile, formatting);
1006
+ }
1007
+ /**
1008
+ * Set a custom email HTML template override in config.
1009
+ * Sets `email.templates[type] = \`...\``.
1010
+ * Uses backtick template literal for multi-line HTML.
1011
+ * If value is empty string, removes the override instead.
1012
+ */
1013
+ export async function setEmailTemplate(opts, type, value) {
1014
+ validateEmailType(type);
1015
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
1016
+ if (!value) {
1017
+ // Remove the override
1018
+ removeNestedProperty(configObj, ['email', 'templates'], type);
1019
+ }
1020
+ else {
1021
+ const templatesBlock = ensureNestedBlock(configObj, ['email', 'templates']);
1022
+ const existing = templatesBlock.getProperty(type);
1023
+ // Escape backticks and ${} in user content for template literal safety
1024
+ const escaped = value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
1025
+ if (existing) {
1026
+ existing.setInitializer(`\`${escaped}\``);
1027
+ }
1028
+ else {
1029
+ templatesBlock.addPropertyAssignment({
1030
+ name: type,
1031
+ initializer: `\`${escaped}\``,
1032
+ });
1033
+ }
1034
+ }
1035
+ saveWithBackup(opts, sourceFile, formatting);
1036
+ }
1037
+ /**
1038
+ * Remove email override(s) for a given type.
1039
+ * field: 'subject' | 'template' | 'both'
1040
+ */
1041
+ export async function removeEmailOverride(opts, type, field) {
1042
+ validateEmailType(type);
1043
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
1044
+ if (field === 'subject' || field === 'both') {
1045
+ removeNestedProperty(configObj, ['email', 'subjects'], type);
1046
+ }
1047
+ if (field === 'template' || field === 'both') {
1048
+ removeNestedProperty(configObj, ['email', 'templates'], type);
1049
+ }
1050
+ saveWithBackup(opts, sourceFile, formatting);
1051
+ }
1052
+ // ─── Per-Locale Email Overrides (i18n) ───
1053
+ /**
1054
+ * Set a per-locale email subject override.
1055
+ * Converts `email.subjects[type]` from string to `{ en: existingStr, [locale]: value }` if needed.
1056
+ */
1057
+ export async function setEmailSubjectForLocale(opts, type, locale, value) {
1058
+ validateEmailType(type);
1059
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
1060
+ setLocalizedProperty(configObj, ['email', 'subjects'], type, locale, value, 'string');
1061
+ saveWithBackup(opts, sourceFile, formatting);
1062
+ }
1063
+ /**
1064
+ * Set a per-locale email template override.
1065
+ * Converts `email.templates[type]` from string to `{ en: existingStr, [locale]: value }` if needed.
1066
+ */
1067
+ export async function setEmailTemplateForLocale(opts, type, locale, value) {
1068
+ validateEmailType(type);
1069
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
1070
+ setLocalizedProperty(configObj, ['email', 'templates'], type, locale, value, 'template');
1071
+ saveWithBackup(opts, sourceFile, formatting);
1072
+ }
1073
+ /**
1074
+ * Remove a per-locale email override.
1075
+ * If the property is an object `{ en: '...', ko: '...' }`, removes the locale key.
1076
+ * If only one key remains after removal, simplifies to a plain string.
1077
+ * If no keys remain, removes the property entirely.
1078
+ */
1079
+ export async function removeEmailOverrideForLocale(opts, type, field, locale) {
1080
+ validateEmailType(type);
1081
+ const { sourceFile, configObj, formatting } = loadAndParse(opts);
1082
+ const section = field === 'subject' ? 'subjects' : 'templates';
1083
+ const parentBlock = getNestedBlock(configObj, ['email', section]);
1084
+ if (!parentBlock)
1085
+ return;
1086
+ const prop = parentBlock.getProperty(type);
1087
+ if (!prop)
1088
+ return;
1089
+ const init = prop.getInitializer();
1090
+ if (!init)
1091
+ return;
1092
+ // If it's an object literal, remove the locale key
1093
+ if (init.getKind() === SyntaxKind.ObjectLiteralExpression) {
1094
+ const obj = init;
1095
+ const localeProp = obj.getProperty(locale);
1096
+ if (localeProp) {
1097
+ localeProp.remove();
1098
+ }
1099
+ // If object is now empty, remove the entire property
1100
+ if (obj.getProperties().length === 0) {
1101
+ removeNestedProperty(configObj, ['email', section], type);
1102
+ }
1103
+ // If only 'en' key remains, simplify to plain string
1104
+ else if (obj.getProperties().length === 1) {
1105
+ const remaining = obj.getProperties()[0];
1106
+ if (remaining.getName() === 'en') {
1107
+ const enValue = remaining.getInitializer()?.getText() ?? "''";
1108
+ prop.setInitializer(enValue);
1109
+ }
1110
+ }
1111
+ }
1112
+ // If it's a plain string and locale is 'en', remove entirely
1113
+ else if (locale === 'en') {
1114
+ removeNestedProperty(configObj, ['email', section], type);
1115
+ }
1116
+ saveWithBackup(opts, sourceFile, formatting);
1117
+ }
1118
+ /**
1119
+ * Set a locale key inside a potentially LocalizedString property.
1120
+ * If property is currently a string → convert to { en: existingStr, [locale]: newValue }.
1121
+ * If property is already an object → add/update the locale key.
1122
+ * If property doesn't exist → create { [locale]: newValue }.
1123
+ */
1124
+ function setLocalizedProperty(configObj, parentPath, key, locale, value, mode) {
1125
+ const parentBlock = ensureNestedBlock(configObj, parentPath);
1126
+ const prop = parentBlock.getProperty(key);
1127
+ const escapeValue = (v) => {
1128
+ if (mode === 'template') {
1129
+ const escaped = v.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
1130
+ return `\`${escaped}\``;
1131
+ }
1132
+ const escaped = v.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
1133
+ return `'${escaped}'`;
1134
+ };
1135
+ if (!prop) {
1136
+ // Property doesn't exist — create as { [locale]: value }
1137
+ parentBlock.addPropertyAssignment({
1138
+ name: key,
1139
+ initializer: `{\n ${locale}: ${escapeValue(value)},\n }`,
1140
+ });
1141
+ return;
1142
+ }
1143
+ const init = prop.getInitializer();
1144
+ if (!init)
1145
+ return;
1146
+ // Already an object literal → add/update the locale key
1147
+ if (init.getKind() === SyntaxKind.ObjectLiteralExpression) {
1148
+ const obj = init;
1149
+ const localeProp = obj.getProperty(locale);
1150
+ if (localeProp) {
1151
+ localeProp.setInitializer(escapeValue(value));
1152
+ }
1153
+ else {
1154
+ obj.addPropertyAssignment({
1155
+ name: locale,
1156
+ initializer: escapeValue(value),
1157
+ });
1158
+ }
1159
+ return;
1160
+ }
1161
+ // Currently a plain string/template literal → convert to object form
1162
+ const existingText = init.getText();
1163
+ if (locale === 'en') {
1164
+ // Replacing the en value: convert to { en: newValue }
1165
+ prop.setInitializer(`{\n en: ${escapeValue(value)},\n }`);
1166
+ }
1167
+ else {
1168
+ // Adding non-en locale: convert to { en: existingStr, [locale]: newValue }
1169
+ prop.setInitializer(`{\n en: ${existingText},\n ${locale}: ${escapeValue(value)},\n }`);
1170
+ }
1171
+ }
1172
+ /**
1173
+ * Get a nested object block (without creating it). Returns undefined if any part of the path is missing.
1174
+ */
1175
+ function getNestedBlock(configObj, path) {
1176
+ let current = configObj;
1177
+ for (const key of path) {
1178
+ const prop = current.getProperty(key);
1179
+ if (!prop)
1180
+ return undefined;
1181
+ const init = prop.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);
1182
+ if (!init)
1183
+ return undefined;
1184
+ current = init;
1185
+ }
1186
+ return current;
1187
+ }
1188
+ //# sourceMappingURL=config-editor.js.map