@xano/cli 0.0.95-beta.12 → 0.0.95-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,5 +15,6 @@ export default class SandboxPush extends BaseCommand {
15
15
  };
16
16
  run(): Promise<void>;
17
17
  private collectFiles;
18
+ private renderBadIndexes;
18
19
  private renderBadReferences;
19
20
  }
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import BaseCommand from '../../../base-command.js';
5
5
  import { findFilesWithGuid } from '../../../utils/document-parser.js';
6
- import { checkReferences } from '../../../utils/reference-checker.js';
6
+ import { checkReferences, checkTableIndexes } from '../../../utils/reference-checker.js';
7
7
  export default class SandboxPush extends BaseCommand {
8
8
  static args = {
9
9
  directory: Args.string({
@@ -72,6 +72,11 @@ Pushed 42 documents to sandbox environment from ./my-workspace
72
72
  if (badRefs.length > 0) {
73
73
  this.renderBadReferences(badRefs);
74
74
  }
75
+ // Check for indexes referencing non-existent schema fields
76
+ const badIndexes = checkTableIndexes(documentEntries);
77
+ if (badIndexes.length > 0) {
78
+ this.renderBadIndexes(badIndexes);
79
+ }
75
80
  const multidoc = documentEntries.map((d) => d.content).join('\n---\n');
76
81
  const queryParams = new URLSearchParams({
77
82
  env: flags.env.toString(),
@@ -150,6 +155,19 @@ Pushed 42 documents to sandbox environment from ./my-workspace
150
155
  }
151
156
  return files.sort();
152
157
  }
158
+ renderBadIndexes(badIndexes) {
159
+ this.log('');
160
+ this.log(ux.colorize('red', ux.colorize('bold', '=== CRITICAL: Invalid Indexes ===')));
161
+ this.log('');
162
+ this.log(ux.colorize('red', 'The following tables have indexes referencing fields that do not exist in the schema.'));
163
+ this.log(ux.colorize('red', 'These will cause the import to fail.'));
164
+ this.log('');
165
+ for (const idx of badIndexes) {
166
+ this.log(` ${ux.colorize('red', 'CRITICAL'.padEnd(16))} ${'table'.padEnd(18)} ${idx.table}`);
167
+ this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', `${idx.indexType} index → field "${idx.field}" does not exist in schema`)}`);
168
+ }
169
+ this.log('');
170
+ }
153
171
  renderBadReferences(badRefs) {
154
172
  this.log('');
155
173
  this.log(ux.colorize('yellow', ux.colorize('bold', '=== Unresolved References ===')));
@@ -57,39 +57,14 @@ Unit tests for tenant my-tenant:
57
57
  if (!workspaceId) {
58
58
  this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
59
59
  }
60
- // Resolve tenant to get its workspace
61
- const tenantUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(flags.tenant)}`;
62
- let tenantWorkspaceId;
63
- try {
64
- const tenantResponse = await this.verboseFetch(tenantUrl, {
65
- headers: {
66
- accept: 'application/json',
67
- Authorization: `Bearer ${profile.access_token}`,
68
- },
69
- method: 'GET',
70
- }, flags.verbose, profile.access_token);
71
- if (!tenantResponse.ok) {
72
- const errorText = await tenantResponse.text();
73
- this.error(`Failed to find tenant '${flags.tenant}': ${tenantResponse.status}\n${errorText}`);
74
- }
75
- const tenant = (await tenantResponse.json());
76
- tenantWorkspaceId = String(tenant.workspace?.id || workspaceId);
77
- }
78
- catch (error) {
79
- if (error instanceof Error) {
80
- this.error(`Failed to resolve tenant: ${error.message}`);
81
- }
82
- else {
83
- this.error(`Failed to resolve tenant: ${String(error)}`);
84
- }
85
- }
60
+ const tenantName = encodeURIComponent(flags.tenant);
86
61
  const params = new URLSearchParams();
87
62
  params.set('per_page', '10000');
88
63
  if (flags.branch)
89
64
  params.set('branch', flags.branch);
90
65
  if (flags['obj-type'])
91
66
  params.set('obj_type', flags['obj-type']);
92
- const apiUrl = `${profile.instance_origin}/api:meta/workspace/${tenantWorkspaceId}/unit_test?${params}`;
67
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/unit_test?${params}`;
93
68
  try {
94
69
  const response = await this.verboseFetch(apiUrl, {
95
70
  headers: {
@@ -53,33 +53,8 @@ Result: PASS
53
53
  if (!workspaceId) {
54
54
  this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
55
55
  }
56
- // Resolve tenant to get its workspace
57
- const tenantUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(flags.tenant)}`;
58
- let tenantWorkspaceId;
59
- try {
60
- const tenantResponse = await this.verboseFetch(tenantUrl, {
61
- headers: {
62
- accept: 'application/json',
63
- Authorization: `Bearer ${profile.access_token}`,
64
- },
65
- method: 'GET',
66
- }, flags.verbose, profile.access_token);
67
- if (!tenantResponse.ok) {
68
- const errorText = await tenantResponse.text();
69
- this.error(`Failed to find tenant '${flags.tenant}': ${tenantResponse.status}\n${errorText}`);
70
- }
71
- const tenant = (await tenantResponse.json());
72
- tenantWorkspaceId = String(tenant.workspace?.id || workspaceId);
73
- }
74
- catch (error) {
75
- if (error instanceof Error) {
76
- this.error(`Failed to resolve tenant: ${error.message}`);
77
- }
78
- else {
79
- this.error(`Failed to resolve tenant: ${String(error)}`);
80
- }
81
- }
82
- const apiUrl = `${profile.instance_origin}/api:meta/workspace/${tenantWorkspaceId}/unit_test/${encodeURIComponent(args.unit_test_id)}/run`;
56
+ const tenantName = encodeURIComponent(flags.tenant);
57
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/unit_test/${encodeURIComponent(args.unit_test_id)}/run`;
83
58
  try {
84
59
  if (flags.output === 'summary') {
85
60
  this.log(`Running unit test ${args.unit_test_id}...`);
@@ -62,33 +62,8 @@ Results: 4 passed, 1 failed
62
62
  if (!workspaceId) {
63
63
  this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
64
64
  }
65
- // Resolve tenant to get its workspace
66
- const tenantUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(flags.tenant)}`;
67
- let tenantWorkspaceId;
68
- try {
69
- const tenantResponse = await this.verboseFetch(tenantUrl, {
70
- headers: {
71
- accept: 'application/json',
72
- Authorization: `Bearer ${profile.access_token}`,
73
- },
74
- method: 'GET',
75
- }, flags.verbose, profile.access_token);
76
- if (!tenantResponse.ok) {
77
- const errorText = await tenantResponse.text();
78
- this.error(`Failed to find tenant '${flags.tenant}': ${tenantResponse.status}\n${errorText}`);
79
- }
80
- const tenant = (await tenantResponse.json());
81
- tenantWorkspaceId = String(tenant.workspace?.id || workspaceId);
82
- }
83
- catch (error) {
84
- if (error instanceof Error) {
85
- this.error(`Failed to resolve tenant: ${error.message}`);
86
- }
87
- else {
88
- this.error(`Failed to resolve tenant: ${String(error)}`);
89
- }
90
- }
91
- const baseUrl = `${profile.instance_origin}/api:meta/workspace/${tenantWorkspaceId}/unit_test`;
65
+ const tenantName = encodeURIComponent(flags.tenant);
66
+ const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/unit_test`;
92
67
  try {
93
68
  const listParams = new URLSearchParams();
94
69
  listParams.set('per_page', '10000');
@@ -52,33 +52,8 @@ Deleted workflow test 42
52
52
  if (!workspaceId) {
53
53
  this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
54
54
  }
55
- // Resolve tenant to get its workspace
56
- const tenantUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(flags.tenant)}`;
57
- let tenantWorkspaceId;
58
- try {
59
- const tenantResponse = await this.verboseFetch(tenantUrl, {
60
- headers: {
61
- accept: 'application/json',
62
- Authorization: `Bearer ${profile.access_token}`,
63
- },
64
- method: 'GET',
65
- }, flags.verbose, profile.access_token);
66
- if (!tenantResponse.ok) {
67
- const errorText = await tenantResponse.text();
68
- this.error(`Failed to find tenant '${flags.tenant}': ${tenantResponse.status}\n${errorText}`);
69
- }
70
- const tenant = (await tenantResponse.json());
71
- tenantWorkspaceId = String(tenant.workspace?.id || workspaceId);
72
- }
73
- catch (error) {
74
- if (error instanceof Error) {
75
- this.error(`Failed to resolve tenant: ${error.message}`);
76
- }
77
- else {
78
- this.error(`Failed to resolve tenant: ${String(error)}`);
79
- }
80
- }
81
- const apiUrl = `${profile.instance_origin}/api:meta/workspace/${tenantWorkspaceId}/workflow_test/${args.workflow_test_id}`;
55
+ const tenantName = encodeURIComponent(flags.tenant);
56
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/workflow_test/${args.workflow_test_id}`;
82
57
  try {
83
58
  const response = await this.verboseFetch(apiUrl, {
84
59
  headers: {
@@ -52,37 +52,12 @@ Workflow tests for tenant my-tenant:
52
52
  if (!workspaceId) {
53
53
  this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
54
54
  }
55
- // Resolve tenant to get its workspace
56
- const tenantUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(flags.tenant)}`;
57
- let tenantWorkspaceId;
58
- try {
59
- const tenantResponse = await this.verboseFetch(tenantUrl, {
60
- headers: {
61
- accept: 'application/json',
62
- Authorization: `Bearer ${profile.access_token}`,
63
- },
64
- method: 'GET',
65
- }, flags.verbose, profile.access_token);
66
- if (!tenantResponse.ok) {
67
- const errorText = await tenantResponse.text();
68
- this.error(`Failed to find tenant '${flags.tenant}': ${tenantResponse.status}\n${errorText}`);
69
- }
70
- const tenant = (await tenantResponse.json());
71
- tenantWorkspaceId = String(tenant.workspace?.id || workspaceId);
72
- }
73
- catch (error) {
74
- if (error instanceof Error) {
75
- this.error(`Failed to resolve tenant: ${error.message}`);
76
- }
77
- else {
78
- this.error(`Failed to resolve tenant: ${String(error)}`);
79
- }
80
- }
55
+ const tenantName = encodeURIComponent(flags.tenant);
81
56
  const params = new URLSearchParams();
82
57
  params.set('per_page', '10000');
83
58
  if (flags.branch)
84
59
  params.set('branch', flags.branch);
85
- const apiUrl = `${profile.instance_origin}/api:meta/workspace/${tenantWorkspaceId}/workflow_test?${params}`;
60
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/workflow_test?${params}`;
86
61
  try {
87
62
  const response = await this.verboseFetch(apiUrl, {
88
63
  headers: {
@@ -53,33 +53,8 @@ Result: PASS (0.25s)
53
53
  if (!workspaceId) {
54
54
  this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
55
55
  }
56
- // Resolve tenant to get its workspace
57
- const tenantUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(flags.tenant)}`;
58
- let tenantWorkspaceId;
59
- try {
60
- const tenantResponse = await this.verboseFetch(tenantUrl, {
61
- headers: {
62
- accept: 'application/json',
63
- Authorization: `Bearer ${profile.access_token}`,
64
- },
65
- method: 'GET',
66
- }, flags.verbose, profile.access_token);
67
- if (!tenantResponse.ok) {
68
- const errorText = await tenantResponse.text();
69
- this.error(`Failed to find tenant '${flags.tenant}': ${tenantResponse.status}\n${errorText}`);
70
- }
71
- const tenant = (await tenantResponse.json());
72
- tenantWorkspaceId = String(tenant.workspace?.id || workspaceId);
73
- }
74
- catch (error) {
75
- if (error instanceof Error) {
76
- this.error(`Failed to resolve tenant: ${error.message}`);
77
- }
78
- else {
79
- this.error(`Failed to resolve tenant: ${String(error)}`);
80
- }
81
- }
82
- const apiUrl = `${profile.instance_origin}/api:meta/workspace/${tenantWorkspaceId}/workflow_test/${args.workflow_test_id}/run`;
56
+ const tenantName = encodeURIComponent(flags.tenant);
57
+ const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/workflow_test/${args.workflow_test_id}/run`;
83
58
  try {
84
59
  if (flags.output === 'summary') {
85
60
  this.log(`Running workflow test ${args.workflow_test_id}...`);
@@ -57,33 +57,8 @@ Results: 2 passed, 1 failed
57
57
  if (!workspaceId) {
58
58
  this.error('No workspace ID provided. Use --workspace flag or set one in your profile.');
59
59
  }
60
- // Resolve tenant to get its workspace
61
- const tenantUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${encodeURIComponent(flags.tenant)}`;
62
- let tenantWorkspaceId;
63
- try {
64
- const tenantResponse = await this.verboseFetch(tenantUrl, {
65
- headers: {
66
- accept: 'application/json',
67
- Authorization: `Bearer ${profile.access_token}`,
68
- },
69
- method: 'GET',
70
- }, flags.verbose, profile.access_token);
71
- if (!tenantResponse.ok) {
72
- const errorText = await tenantResponse.text();
73
- this.error(`Failed to find tenant '${flags.tenant}': ${tenantResponse.status}\n${errorText}`);
74
- }
75
- const tenant = (await tenantResponse.json());
76
- tenantWorkspaceId = String(tenant.workspace?.id || workspaceId);
77
- }
78
- catch (error) {
79
- if (error instanceof Error) {
80
- this.error(`Failed to resolve tenant: ${error.message}`);
81
- }
82
- else {
83
- this.error(`Failed to resolve tenant: ${String(error)}`);
84
- }
85
- }
86
- const baseUrl = `${profile.instance_origin}/api:meta/workspace/${tenantWorkspaceId}/workflow_test`;
60
+ const tenantName = encodeURIComponent(flags.tenant);
61
+ const baseUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/tenant/${tenantName}/workflow_test`;
87
62
  try {
88
63
  const listParams = new URLSearchParams();
89
64
  listParams.set('per_page', '10000');
@@ -30,6 +30,7 @@ export default class Push extends BaseCommand {
30
30
  * type subdirectory name then filename for deterministic ordering.
31
31
  */
32
32
  private collectFiles;
33
+ private renderBadIndexes;
33
34
  private renderBadReferences;
34
35
  private loadCredentials;
35
36
  }
@@ -6,7 +6,7 @@ import * as os from 'node:os';
6
6
  import * as path from 'node:path';
7
7
  import BaseCommand from '../../../base-command.js';
8
8
  import { buildDocumentKey, findFilesWithGuid, parseDocument } from '../../../utils/document-parser.js';
9
- import { checkReferences } from '../../../utils/reference-checker.js';
9
+ import { checkReferences, checkTableIndexes } from '../../../utils/reference-checker.js';
10
10
  export default class Push extends BaseCommand {
11
11
  static args = {
12
12
  directory: Args.string({
@@ -331,6 +331,11 @@ Push functions but exclude test files
331
331
  if (badRefs.length > 0) {
332
332
  this.renderBadReferences(badRefs);
333
333
  }
334
+ // Check for indexes referencing non-existent schema fields
335
+ const badIndexes = checkTableIndexes(documentEntries);
336
+ if (badIndexes.length > 0) {
337
+ this.renderBadIndexes(badIndexes);
338
+ }
334
339
  // Check for critical errors that must block the push
335
340
  const criticalOps = preview.operations.filter((op) => op.details?.includes('exception:') || op.details?.includes('mvp:placeholder'));
336
341
  if (criticalOps.length > 0) {
@@ -751,6 +756,19 @@ Push functions but exclude test files
751
756
  }
752
757
  return files.sort();
753
758
  }
759
+ renderBadIndexes(badIndexes) {
760
+ this.log('');
761
+ this.log(ux.colorize('red', ux.colorize('bold', '=== CRITICAL: Invalid Indexes ===')));
762
+ this.log('');
763
+ this.log(ux.colorize('red', 'The following tables have indexes referencing fields that do not exist in the schema.'));
764
+ this.log(ux.colorize('red', 'These will cause the import to fail.'));
765
+ this.log('');
766
+ for (const idx of badIndexes) {
767
+ this.log(` ${ux.colorize('red', 'CRITICAL'.padEnd(16))} ${'table'.padEnd(18)} ${idx.table}`);
768
+ this.log(` ${' '.repeat(16)} ${' '.repeat(18)} ${ux.colorize('dim', `${idx.indexType} index → field "${idx.field}" does not exist in schema`)}`);
769
+ }
770
+ this.log('');
771
+ }
754
772
  renderBadReferences(badRefs) {
755
773
  this.log(ux.colorize('yellow', ux.colorize('bold', '=== Unresolved References ===')));
756
774
  this.log('');
@@ -43,3 +43,15 @@ export declare function checkReferences(documents: Array<{
43
43
  name: string;
44
44
  type: string;
45
45
  }>): BadReference[];
46
+ export interface BadIndex {
47
+ field: string;
48
+ indexType: string;
49
+ table: string;
50
+ }
51
+ /**
52
+ * Check table documents for indexes that reference fields not in the schema.
53
+ * Parses the XanoScript table format to extract schema field names and index field names.
54
+ */
55
+ export declare function checkTableIndexes(documents: Array<{
56
+ content: string;
57
+ }>): BadIndex[];
@@ -143,3 +143,90 @@ export function checkReferences(documents, serverOperations) {
143
143
  }
144
144
  return badRefs;
145
145
  }
146
+ /**
147
+ * Check table documents for indexes that reference fields not in the schema.
148
+ * Parses the XanoScript table format to extract schema field names and index field names.
149
+ */
150
+ export function checkTableIndexes(documents) {
151
+ const badIndexes = [];
152
+ for (const doc of documents) {
153
+ const parsed = parseDocument(doc.content);
154
+ if (!parsed || parsed.type !== 'table')
155
+ continue;
156
+ const schemaFields = extractSchemaFields(doc.content);
157
+ const indexes = extractIndexes(doc.content);
158
+ for (const idx of indexes) {
159
+ for (const field of idx.fields) {
160
+ if (!schemaFields.has(field)) {
161
+ badIndexes.push({
162
+ field,
163
+ indexType: idx.type,
164
+ table: parsed.name,
165
+ });
166
+ }
167
+ }
168
+ }
169
+ }
170
+ return badIndexes;
171
+ }
172
+ function extractSchemaFields(content) {
173
+ // id and created_at are auto-added during import
174
+ const fields = new Set(['id', 'created_at']);
175
+ // Find the schema block by matching braces
176
+ const schemaStart = content.match(/\bschema\s*\{/);
177
+ if (!schemaStart || schemaStart.index === undefined)
178
+ return fields;
179
+ let depth = 0;
180
+ let blockStart = -1;
181
+ let blockEnd = -1;
182
+ for (let i = schemaStart.index; i < content.length; i++) {
183
+ if (content[i] === '{') {
184
+ if (depth === 0)
185
+ blockStart = i + 1;
186
+ depth++;
187
+ }
188
+ else if (content[i] === '}') {
189
+ depth--;
190
+ if (depth === 0) {
191
+ blockEnd = i;
192
+ break;
193
+ }
194
+ }
195
+ }
196
+ if (blockStart < 0 || blockEnd < 0)
197
+ return fields;
198
+ const schemaBlock = content.slice(blockStart, blockEnd);
199
+ // Match field declarations: "type name" or "type name?" or "type name?=default"
200
+ const fieldRegex = /^\s*\w+\s+(\w+)[?\s{]/gm;
201
+ let match;
202
+ while ((match = fieldRegex.exec(schemaBlock)) !== null) {
203
+ fields.add(match[1]);
204
+ }
205
+ return fields;
206
+ }
207
+ function extractIndexes(content) {
208
+ const indexes = [];
209
+ // Match the index array: index = [ ... ]
210
+ const indexMatch = content.match(/\bindex\s*=\s*\[([\s\S]*?)\n\s*\]/);
211
+ if (!indexMatch)
212
+ return indexes;
213
+ const indexBlock = indexMatch[1];
214
+ // Match each index object: {type: "btree", field: [{name: "col", op: "desc"}]}
215
+ const entryRegex = /\{([^}]+)\}/g;
216
+ let match;
217
+ while ((match = entryRegex.exec(indexBlock)) !== null) {
218
+ const entry = match[1];
219
+ const typeMatch = entry.match(/type:\s*"(\w+)"/);
220
+ const type = typeMatch ? typeMatch[1] : 'unknown';
221
+ const fieldNames = [];
222
+ const nameRegex = /name:\s*"(\w*)"/g;
223
+ let nameMatch;
224
+ while ((nameMatch = nameRegex.exec(entry)) !== null) {
225
+ fieldNames.push(nameMatch[1]);
226
+ }
227
+ if (fieldNames.length > 0) {
228
+ indexes.push({ fields: fieldNames, type });
229
+ }
230
+ }
231
+ return indexes;
232
+ }