intellitester 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +200 -0
- package/dist/chunk-35WJGNDA.cjs +136 -0
- package/dist/chunk-35WJGNDA.cjs.map +1 -0
- package/dist/chunk-4B54JUOP.js +234 -0
- package/dist/chunk-4B54JUOP.js.map +1 -0
- package/dist/chunk-5LFSLMQ7.js +2517 -0
- package/dist/chunk-5LFSLMQ7.js.map +1 -0
- package/dist/chunk-6PYKWWH5.js +63 -0
- package/dist/chunk-6PYKWWH5.js.map +1 -0
- package/dist/chunk-ARJYJVRM.cjs +302 -0
- package/dist/chunk-ARJYJVRM.cjs.map +1 -0
- package/dist/chunk-CN6HSJJX.js +133 -0
- package/dist/chunk-CN6HSJJX.js.map +1 -0
- package/dist/chunk-DE5UFTTG.js +31 -0
- package/dist/chunk-DE5UFTTG.js.map +1 -0
- package/dist/chunk-ECBA4GJ3.js +287 -0
- package/dist/chunk-ECBA4GJ3.js.map +1 -0
- package/dist/chunk-OFXNJXMV.cjs +237 -0
- package/dist/chunk-OFXNJXMV.cjs.map +1 -0
- package/dist/chunk-PAKODOH4.cjs +66 -0
- package/dist/chunk-PAKODOH4.cjs.map +1 -0
- package/dist/chunk-QMYM2TCH.cjs +36 -0
- package/dist/chunk-QMYM2TCH.cjs.map +1 -0
- package/dist/chunk-SAVY6D3X.js +125 -0
- package/dist/chunk-SAVY6D3X.js.map +1 -0
- package/dist/chunk-UUJXCHVT.cjs +128 -0
- package/dist/chunk-UUJXCHVT.cjs.map +1 -0
- package/dist/chunk-XWGUA67E.cjs +2552 -0
- package/dist/chunk-XWGUA67E.cjs.map +1 -0
- package/dist/cli/index.cjs +1985 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1957 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/cleanup/index.cjs +45 -0
- package/dist/core/cleanup/index.cjs.map +1 -0
- package/dist/core/cleanup/index.d.cts +117 -0
- package/dist/core/cleanup/index.d.ts +117 -0
- package/dist/core/cleanup/index.js +8 -0
- package/dist/core/cleanup/index.js.map +1 -0
- package/dist/index.cjs +110 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +852 -0
- package/dist/index.d.ts +852 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/integration/index.cjs +22 -0
- package/dist/integration/index.cjs.map +1 -0
- package/dist/integration/index.d.cts +42 -0
- package/dist/integration/index.d.ts +42 -0
- package/dist/integration/index.js +20 -0
- package/dist/integration/index.js.map +1 -0
- package/dist/providers/appwrite/index.cjs +16 -0
- package/dist/providers/appwrite/index.cjs.map +1 -0
- package/dist/providers/appwrite/index.d.cts +12 -0
- package/dist/providers/appwrite/index.d.ts +12 -0
- package/dist/providers/appwrite/index.js +3 -0
- package/dist/providers/appwrite/index.js.map +1 -0
- package/dist/providers/index.cjs +60 -0
- package/dist/providers/index.cjs.map +1 -0
- package/dist/providers/index.d.cts +13 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.js +7 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/mysql/index.cjs +16 -0
- package/dist/providers/mysql/index.cjs.map +1 -0
- package/dist/providers/mysql/index.d.cts +14 -0
- package/dist/providers/mysql/index.d.ts +14 -0
- package/dist/providers/mysql/index.js +3 -0
- package/dist/providers/mysql/index.js.map +1 -0
- package/dist/providers/postgres/index.cjs +16 -0
- package/dist/providers/postgres/index.cjs.map +1 -0
- package/dist/providers/postgres/index.d.cts +10 -0
- package/dist/providers/postgres/index.d.ts +10 -0
- package/dist/providers/postgres/index.js +3 -0
- package/dist/providers/postgres/index.js.map +1 -0
- package/dist/providers/sqlite/index.cjs +16 -0
- package/dist/providers/sqlite/index.cjs.map +1 -0
- package/dist/providers/sqlite/index.d.cts +11 -0
- package/dist/providers/sqlite/index.d.ts +11 -0
- package/dist/providers/sqlite/index.js +3 -0
- package/dist/providers/sqlite/index.js.map +1 -0
- package/dist/types-LONNVTIF.d.cts +56 -0
- package/dist/types-l-ZaFKC-.d.ts +56 -0
- package/package.json +114 -0
- package/schemas/intellitester.config.schema.json +384 -0
- package/schemas/test.schema.json +517 -0
- package/schemas/workflow.schema.json +227 -0
package/README.md
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# IntelliTester
|
|
2
|
+
|
|
3
|
+
Uses AI + iOS or Android emulators / web browsers to create automated test suites using simple instructions
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
Before running web tests with IntelliTester, you need to install Playwright browsers:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx playwright install chromium
|
|
11
|
+
# Or for all browsers:
|
|
12
|
+
npx playwright install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Editor Configuration
|
|
16
|
+
|
|
17
|
+
IntelliTester provides JSON schemas for YAML configuration files to enable autocomplete and validation in VS Code and other editors.
|
|
18
|
+
|
|
19
|
+
### VS Code Setup
|
|
20
|
+
|
|
21
|
+
Add this to your `.vscode/settings.json` (or workspace settings):
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"yaml.schemas": {
|
|
26
|
+
"./node_modules/intellitester/schemas/intellitester.config.schema.json": "intellitester.config.yaml",
|
|
27
|
+
"./node_modules/intellitester/schemas/test.schema.json": "*.test.yaml",
|
|
28
|
+
"./node_modules/intellitester/schemas/workflow.schema.json": "*.workflow.yaml"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This enables:
|
|
34
|
+
- Auto-completion for all configuration properties
|
|
35
|
+
- Inline documentation and examples
|
|
36
|
+
- Real-time validation and error checking
|
|
37
|
+
- Type checking for action steps and configurations
|
|
38
|
+
|
|
39
|
+
### Using Local Development Schemas
|
|
40
|
+
|
|
41
|
+
If you're working on IntelliTester itself or want to use local schemas, use these paths instead:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"yaml.schemas": {
|
|
46
|
+
"./schemas/intellitester.config.schema.json": "intellitester.config.yaml",
|
|
47
|
+
"./schemas/test.schema.json": "*.test.yaml",
|
|
48
|
+
"./schemas/workflow.schema.json": "*.workflow.yaml"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Debugging
|
|
54
|
+
|
|
55
|
+
IntelliTester provides powerful debugging capabilities to help troubleshoot failing tests.
|
|
56
|
+
|
|
57
|
+
### Debug Mode
|
|
58
|
+
|
|
59
|
+
Run tests with the `--debug` flag to pause execution on failure and open the Playwright Inspector:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Run with debug mode - pauses on failure
|
|
63
|
+
intellitester run tests/login.test.yaml --headed --debug
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Debug Action Type
|
|
67
|
+
|
|
68
|
+
Add breakpoints directly in your test files using the `debug` action type. This pauses execution and opens the Playwright Inspector at that specific step:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
# Add breakpoints in your test
|
|
72
|
+
steps:
|
|
73
|
+
- type: navigate
|
|
74
|
+
value: /signup
|
|
75
|
+
- type: debug # Pauses here, opens Playwright Inspector
|
|
76
|
+
- type: input
|
|
77
|
+
target: { testId: email }
|
|
78
|
+
value: test@example.com
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
When execution pauses, you can:
|
|
82
|
+
- Inspect the current page state
|
|
83
|
+
- Step through actions manually
|
|
84
|
+
- Examine selectors and elements
|
|
85
|
+
- Continue execution when ready
|
|
86
|
+
|
|
87
|
+
## Resource Cleanup
|
|
88
|
+
|
|
89
|
+
IntelliTester automatically tracks resources created during tests and cleans them up after execution. This ensures tests don't leave behind database rows, files, users, or other artifacts.
|
|
90
|
+
|
|
91
|
+
### Built-in Providers
|
|
92
|
+
|
|
93
|
+
IntelliTester includes cleanup providers for common backends:
|
|
94
|
+
|
|
95
|
+
- **Appwrite** - Delete rows, files, teams, users, memberships
|
|
96
|
+
- **PostgreSQL** - Delete rows and users
|
|
97
|
+
- **MySQL** - Delete rows and users
|
|
98
|
+
- **SQLite** - Delete rows and users
|
|
99
|
+
|
|
100
|
+
### Configuration
|
|
101
|
+
|
|
102
|
+
Configure cleanup in your test YAML files or global config:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
# Using Appwrite (backwards compatible)
|
|
106
|
+
appwrite:
|
|
107
|
+
endpoint: https://cloud.appwrite.io/v1
|
|
108
|
+
projectId: ${APPWRITE_PROJECT_ID}
|
|
109
|
+
apiKey: ${APPWRITE_API_KEY}
|
|
110
|
+
cleanup: true
|
|
111
|
+
cleanupOnFailure: true
|
|
112
|
+
|
|
113
|
+
# Or using the new cleanup config
|
|
114
|
+
cleanup:
|
|
115
|
+
provider: appwrite
|
|
116
|
+
parallel: false # Sequential cleanup by default
|
|
117
|
+
retries: 3 # Retry failed deletions
|
|
118
|
+
|
|
119
|
+
# Provider-specific configuration
|
|
120
|
+
appwrite:
|
|
121
|
+
endpoint: ${APPWRITE_ENDPOINT}
|
|
122
|
+
projectId: ${APPWRITE_PROJECT_ID}
|
|
123
|
+
apiKey: ${APPWRITE_API_KEY}
|
|
124
|
+
|
|
125
|
+
# Map resource types to cleanup methods
|
|
126
|
+
types:
|
|
127
|
+
row: appwrite.deleteRow
|
|
128
|
+
team: appwrite.deleteTeam
|
|
129
|
+
stripe_customer: stripe.deleteCustomer
|
|
130
|
+
|
|
131
|
+
# Explicit handler files to load
|
|
132
|
+
handlers:
|
|
133
|
+
- ./src/cleanup/stripe.ts
|
|
134
|
+
|
|
135
|
+
# Auto-discover handlers (enabled by default)
|
|
136
|
+
discover:
|
|
137
|
+
enabled: true
|
|
138
|
+
paths:
|
|
139
|
+
- ./tests/cleanup
|
|
140
|
+
pattern: "**/*.ts"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Custom Cleanup Handlers
|
|
144
|
+
|
|
145
|
+
Create custom handlers for resources not covered by built-in providers:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// intellitester.cleanup.ts (auto-discovered at project root)
|
|
149
|
+
import { defineCleanupHandlers } from 'intellitester/cleanup';
|
|
150
|
+
import Stripe from 'stripe';
|
|
151
|
+
|
|
152
|
+
export default defineCleanupHandlers({
|
|
153
|
+
stripe_customer: async (resource) => {
|
|
154
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
155
|
+
await stripe.customers.del(resource.id);
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
stripe_subscription: async (resource) => {
|
|
159
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
160
|
+
await stripe.subscriptions.cancel(resource.id);
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Handlers are loaded in this order (later definitions override earlier ones):
|
|
166
|
+
|
|
167
|
+
1. Built-in provider methods (e.g., `appwrite.deleteRow`)
|
|
168
|
+
2. `intellitester.cleanup.ts` at project root
|
|
169
|
+
3. Files in discovery paths (default: `tests/cleanup/**/*.ts`)
|
|
170
|
+
4. Explicit handler files from config
|
|
171
|
+
|
|
172
|
+
### Tracking Resources
|
|
173
|
+
|
|
174
|
+
In your app's server-side code, track resources for cleanup:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { track } from 'intellitester/integration';
|
|
178
|
+
|
|
179
|
+
// Track a database row
|
|
180
|
+
await track({
|
|
181
|
+
type: 'row',
|
|
182
|
+
id: row.$id,
|
|
183
|
+
databaseId: 'main',
|
|
184
|
+
tableId: 'users',
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Track a team
|
|
188
|
+
await track({
|
|
189
|
+
type: 'team',
|
|
190
|
+
id: team.$id,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Track a custom resource (requires custom handler)
|
|
194
|
+
await track({
|
|
195
|
+
type: 'stripe_customer',
|
|
196
|
+
id: customer.id,
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The `track()` function is production-safe - it's a no-op if the required environment variables aren't set. IntelliTester sets these automatically during test execution.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/providers/mysql/index.ts
|
|
4
|
+
function createMysqlProvider(config) {
|
|
5
|
+
let connection = null;
|
|
6
|
+
const methods = {
|
|
7
|
+
deleteRow: async (resource) => {
|
|
8
|
+
if (!connection) {
|
|
9
|
+
throw new Error("MySQL connection not initialized. Call configure() first.");
|
|
10
|
+
}
|
|
11
|
+
const table = resource.table;
|
|
12
|
+
const database = resource.database || config.database;
|
|
13
|
+
if (!table) {
|
|
14
|
+
throw new Error(`Missing table name for row ${resource.id}`);
|
|
15
|
+
}
|
|
16
|
+
await connection.execute(
|
|
17
|
+
`DELETE FROM \`${database}\`.\`${table}\` WHERE id = ?`,
|
|
18
|
+
[resource.id]
|
|
19
|
+
);
|
|
20
|
+
},
|
|
21
|
+
deleteUser: async (resource) => {
|
|
22
|
+
if (!connection) {
|
|
23
|
+
throw new Error("MySQL connection not initialized. Call configure() first.");
|
|
24
|
+
}
|
|
25
|
+
const table = resource.table || "users";
|
|
26
|
+
const database = resource.database || config.database;
|
|
27
|
+
await connection.execute(
|
|
28
|
+
`DELETE FROM \`${database}\`.\`${table}\` WHERE id = ?`,
|
|
29
|
+
[resource.id]
|
|
30
|
+
);
|
|
31
|
+
},
|
|
32
|
+
customDelete: async (resource) => {
|
|
33
|
+
if (!connection) {
|
|
34
|
+
throw new Error("MySQL connection not initialized. Call configure() first.");
|
|
35
|
+
}
|
|
36
|
+
const query = resource.query;
|
|
37
|
+
const params = resource.params || [resource.id];
|
|
38
|
+
if (!query) {
|
|
39
|
+
throw new Error(`Missing query for custom delete of resource ${resource.id}`);
|
|
40
|
+
}
|
|
41
|
+
await connection.execute(query, params);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
async function cleanupUntracked(options) {
|
|
45
|
+
if (!connection) {
|
|
46
|
+
throw new Error("MySQL connection not initialized. Call configure() first.");
|
|
47
|
+
}
|
|
48
|
+
const { testStartTime, userId } = options;
|
|
49
|
+
const deleted = [];
|
|
50
|
+
const failed = [];
|
|
51
|
+
let scanned = 0;
|
|
52
|
+
const [tablesRows] = await connection.execute(`
|
|
53
|
+
SELECT TABLE_NAME as table_name
|
|
54
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
55
|
+
WHERE TABLE_SCHEMA = ?
|
|
56
|
+
AND TABLE_TYPE = 'BASE TABLE'
|
|
57
|
+
AND TABLE_NAME NOT LIKE '_intellitester%'
|
|
58
|
+
`, [config.database]);
|
|
59
|
+
for (const row of tablesRows) {
|
|
60
|
+
const tableName = row.table_name;
|
|
61
|
+
scanned++;
|
|
62
|
+
const [columnsRows] = await connection.execute(`
|
|
63
|
+
SELECT COLUMN_NAME as column_name
|
|
64
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
65
|
+
WHERE TABLE_SCHEMA = ?
|
|
66
|
+
AND TABLE_NAME = ?
|
|
67
|
+
`, [config.database, tableName]);
|
|
68
|
+
const columns = columnsRows.map((r) => r.column_name);
|
|
69
|
+
const hasCreatedAt = columns.some((c) => ["created_at", "createdat", "created"].includes(c.toLowerCase()));
|
|
70
|
+
const userIdColumn = columns.find((c) => ["user_id", "userid", "owner_id", "author_id"].includes(c.toLowerCase()));
|
|
71
|
+
if (!hasCreatedAt) continue;
|
|
72
|
+
const createdAtCol = columns.find((c) => ["created_at", "createdat", "created"].includes(c.toLowerCase()));
|
|
73
|
+
if (!createdAtCol) continue;
|
|
74
|
+
let selectQuery = `SELECT id FROM \`${tableName}\` WHERE `;
|
|
75
|
+
const conditions = [];
|
|
76
|
+
const params = [];
|
|
77
|
+
conditions.push(`\`${createdAtCol}\` >= ?`);
|
|
78
|
+
params.push(testStartTime);
|
|
79
|
+
if (userId && userIdColumn) {
|
|
80
|
+
conditions.push(`\`${userIdColumn}\` = ?`);
|
|
81
|
+
params.push(userId);
|
|
82
|
+
}
|
|
83
|
+
selectQuery += conditions.join(" AND ");
|
|
84
|
+
try {
|
|
85
|
+
const [rowsToDelete] = await connection.execute(selectQuery, params);
|
|
86
|
+
const idsToDelete = rowsToDelete.map((r) => r.id);
|
|
87
|
+
if (idsToDelete.length === 0) continue;
|
|
88
|
+
const deleteQuery = `DELETE FROM \`${tableName}\` WHERE ` + conditions.join(" AND ");
|
|
89
|
+
await connection.execute(deleteQuery, params);
|
|
90
|
+
for (const id of idsToDelete) {
|
|
91
|
+
deleted.push(`${tableName}:${id}`);
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
failed.push(`${tableName}:error`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
success: failed.length === 0,
|
|
99
|
+
scanned,
|
|
100
|
+
deleted,
|
|
101
|
+
failed
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
name: "mysql",
|
|
106
|
+
async configure() {
|
|
107
|
+
try {
|
|
108
|
+
const mysql = await import('mysql2/promise');
|
|
109
|
+
const createConnection = mysql.createConnection || mysql.default?.createConnection;
|
|
110
|
+
connection = await createConnection({
|
|
111
|
+
host: config.host,
|
|
112
|
+
port: config.port || 3306,
|
|
113
|
+
user: config.user,
|
|
114
|
+
password: config.password,
|
|
115
|
+
database: config.database
|
|
116
|
+
});
|
|
117
|
+
} catch {
|
|
118
|
+
throw new Error(
|
|
119
|
+
'Failed to initialize MySQL connection. Make sure the "mysql2" package is installed: npm install mysql2'
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
methods,
|
|
124
|
+
cleanupUntracked
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
var mysqlTypeMappings = {
|
|
128
|
+
row: "mysql.deleteRow",
|
|
129
|
+
user: "mysql.deleteUser",
|
|
130
|
+
custom: "mysql.customDelete"
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
exports.createMysqlProvider = createMysqlProvider;
|
|
134
|
+
exports.mysqlTypeMappings = mysqlTypeMappings;
|
|
135
|
+
//# sourceMappingURL=chunk-35WJGNDA.cjs.map
|
|
136
|
+
//# sourceMappingURL=chunk-35WJGNDA.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/providers/mysql/index.ts"],"names":[],"mappings":"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","file":"chunk-35WJGNDA.cjs","sourcesContent":["import type { CleanupProvider, CleanupHandler, CleanupUntrackedOptions, CleanupUntrackedResult } from '../../core/cleanup/types.js';\nimport type { TrackedResource } from '../../integration/index.js';\n\ninterface MysqlConfig {\n host: string;\n port?: number;\n user: string;\n password: string;\n database: string;\n}\n\nexport function createMysqlProvider(config: MysqlConfig): CleanupProvider {\n // Connection will be lazily initialized in configure()\n let connection: any = null;\n\n const methods: Record<string, CleanupHandler> = {\n deleteRow: async (resource: TrackedResource) => {\n if (!connection) {\n throw new Error('MySQL connection not initialized. Call configure() first.');\n }\n\n const table = resource.table as string;\n const database = (resource.database as string) || config.database;\n\n if (!table) {\n throw new Error(`Missing table name for row ${resource.id}`);\n }\n\n // Use parameterized query to prevent SQL injection\n await connection.execute(\n `DELETE FROM \\`${database}\\`.\\`${table}\\` WHERE id = ?`,\n [resource.id]\n );\n },\n\n deleteUser: async (resource: TrackedResource) => {\n if (!connection) {\n throw new Error('MySQL connection not initialized. Call configure() first.');\n }\n\n const table = (resource.table as string) || 'users';\n const database = (resource.database as string) || config.database;\n\n await connection.execute(\n `DELETE FROM \\`${database}\\`.\\`${table}\\` WHERE id = ?`,\n [resource.id]\n );\n },\n\n customDelete: async (resource: TrackedResource) => {\n if (!connection) {\n throw new Error('MySQL connection not initialized. Call configure() first.');\n }\n\n // Allow custom SQL queries via the query property\n const query = resource.query as string;\n const params = (resource.params as any[]) || [resource.id];\n\n if (!query) {\n throw new Error(`Missing query for custom delete of resource ${resource.id}`);\n }\n\n await connection.execute(query, params);\n },\n };\n\n async function cleanupUntracked(options: CleanupUntrackedOptions): Promise<CleanupUntrackedResult> {\n if (!connection) {\n throw new Error('MySQL connection not initialized. Call configure() first.');\n }\n\n const { testStartTime, userId } = options;\n const deleted: string[] = [];\n const failed: string[] = [];\n let scanned = 0;\n\n // 1. Get all tables in the database\n const [tablesRows] = await connection.execute(`\n SELECT TABLE_NAME as table_name\n FROM INFORMATION_SCHEMA.TABLES\n WHERE TABLE_SCHEMA = ?\n AND TABLE_TYPE = 'BASE TABLE'\n AND TABLE_NAME NOT LIKE '_intellitester%'\n `, [config.database]);\n\n for (const row of tablesRows as { table_name: string }[]) {\n const tableName = row.table_name;\n scanned++;\n\n // 2. Check if table has created_at and user_id columns\n const [columnsRows] = await connection.execute(`\n SELECT COLUMN_NAME as column_name\n FROM INFORMATION_SCHEMA.COLUMNS\n WHERE TABLE_SCHEMA = ?\n AND TABLE_NAME = ?\n `, [config.database, tableName]);\n\n const columns: string[] = (columnsRows as { column_name: string }[]).map(r => r.column_name);\n const hasCreatedAt = columns.some(c => ['created_at', 'createdat', 'created'].includes(c.toLowerCase()));\n const userIdColumn = columns.find(c => ['user_id', 'userid', 'owner_id', 'author_id'].includes(c.toLowerCase()));\n\n if (!hasCreatedAt) continue;\n\n // 3. Find the created_at column name\n const createdAtCol = columns.find(c => ['created_at', 'createdat', 'created'].includes(c.toLowerCase()));\n if (!createdAtCol) continue;\n\n // 4. First, select rows to be deleted (MySQL doesn't have RETURNING)\n let selectQuery = `SELECT id FROM \\`${tableName}\\` WHERE `;\n const conditions: string[] = [];\n const params: (string | undefined)[] = [];\n\n // Add created_at condition\n conditions.push(`\\`${createdAtCol}\\` >= ?`);\n params.push(testStartTime);\n\n // Add user_id condition if available\n if (userId && userIdColumn) {\n conditions.push(`\\`${userIdColumn}\\` = ?`);\n params.push(userId);\n }\n\n selectQuery += conditions.join(' AND ');\n\n try {\n // Get IDs of rows to be deleted\n const [rowsToDelete] = await connection.execute(selectQuery, params);\n const idsToDelete = (rowsToDelete as { id: string | number }[]).map(r => r.id);\n\n if (idsToDelete.length === 0) continue;\n\n // Delete the rows\n const deleteQuery = `DELETE FROM \\`${tableName}\\` WHERE ` + conditions.join(' AND ');\n await connection.execute(deleteQuery, params);\n\n // Record deleted IDs\n for (const id of idsToDelete) {\n deleted.push(`${tableName}:${id}`);\n }\n } catch {\n failed.push(`${tableName}:error`);\n }\n }\n\n return {\n success: failed.length === 0,\n scanned,\n deleted,\n failed,\n };\n }\n\n return {\n name: 'mysql',\n async configure() {\n try {\n // Dynamic import since mysql2 is an optional dependency\n // @ts-expect-error - mysql2 is an optional peer dependency\n const mysql = await import('mysql2/promise');\n const createConnection = mysql.createConnection || mysql.default?.createConnection;\n connection = await createConnection({\n host: config.host,\n port: config.port || 3306,\n user: config.user,\n password: config.password,\n database: config.database,\n });\n } catch {\n throw new Error(\n 'Failed to initialize MySQL connection. Make sure the \"mysql2\" package is installed: npm install mysql2'\n );\n }\n },\n methods,\n cleanupUntracked,\n };\n}\n\n// Default type mappings for MySQL resources\nexport const mysqlTypeMappings: Record<string, string> = {\n row: 'mysql.deleteRow',\n user: 'mysql.deleteUser',\n custom: 'mysql.customDelete',\n};\n"]}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Client, TablesDB, Storage, Teams, Users, Query } from 'node-appwrite';
|
|
2
|
+
|
|
3
|
+
// src/providers/appwrite/index.ts
|
|
4
|
+
function createAppwriteProvider(config) {
|
|
5
|
+
const client = new Client().setEndpoint(config.endpoint).setProject(config.projectId).setKey(config.apiKey);
|
|
6
|
+
const tablesDB = new TablesDB(client);
|
|
7
|
+
const storage = new Storage(client);
|
|
8
|
+
const teams = new Teams(client);
|
|
9
|
+
const users = new Users(client);
|
|
10
|
+
const methods = {
|
|
11
|
+
deleteRow: async (resource) => {
|
|
12
|
+
const databaseId = resource.databaseId;
|
|
13
|
+
const tableId = resource.tableId;
|
|
14
|
+
if (!databaseId || !tableId) {
|
|
15
|
+
throw new Error(`Missing databaseId or tableId for row ${resource.id}`);
|
|
16
|
+
}
|
|
17
|
+
await tablesDB.deleteRow({
|
|
18
|
+
databaseId,
|
|
19
|
+
tableId,
|
|
20
|
+
rowId: resource.id
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
deleteFile: async (resource) => {
|
|
24
|
+
const bucketId = resource.bucketId;
|
|
25
|
+
if (!bucketId) {
|
|
26
|
+
throw new Error(`Missing bucketId for file ${resource.id}`);
|
|
27
|
+
}
|
|
28
|
+
await storage.deleteFile(bucketId, resource.id);
|
|
29
|
+
},
|
|
30
|
+
deleteTeam: async (resource) => {
|
|
31
|
+
await teams.delete(resource.id);
|
|
32
|
+
},
|
|
33
|
+
deleteUser: async (resource) => {
|
|
34
|
+
await users.delete(resource.id);
|
|
35
|
+
},
|
|
36
|
+
deleteMembership: async (resource) => {
|
|
37
|
+
const teamId = resource.teamId;
|
|
38
|
+
if (!teamId) {
|
|
39
|
+
throw new Error(`Missing teamId for membership ${resource.id}`);
|
|
40
|
+
}
|
|
41
|
+
await teams.deleteMembership(teamId, resource.id);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
async function cleanupUntracked(options) {
|
|
45
|
+
const { testStartTime, userId, sessionId } = options;
|
|
46
|
+
const deleted = [];
|
|
47
|
+
const failed = [];
|
|
48
|
+
let scanned = 0;
|
|
49
|
+
console.log(
|
|
50
|
+
`[Appwrite Cleanup] Starting untracked cleanup for session ${sessionId || "unknown"}`
|
|
51
|
+
);
|
|
52
|
+
console.log(`[Appwrite Cleanup] Test start time: ${testStartTime}`);
|
|
53
|
+
console.log(`[Appwrite Cleanup] User ID to match: ${userId || "none"}`);
|
|
54
|
+
try {
|
|
55
|
+
const databases = await tablesDB.list();
|
|
56
|
+
console.log(
|
|
57
|
+
`[Appwrite Cleanup] Found ${databases.databases.length} databases to scan`
|
|
58
|
+
);
|
|
59
|
+
for (const db of databases.databases) {
|
|
60
|
+
const tables = await tablesDB.listTables({ databaseId: db.$id });
|
|
61
|
+
console.log(
|
|
62
|
+
`[Appwrite Cleanup] Database "${db.name}" (${db.$id}): ${tables.tables.length} tables`
|
|
63
|
+
);
|
|
64
|
+
for (const table of tables.tables) {
|
|
65
|
+
if (table.name.startsWith("_intellitester")) {
|
|
66
|
+
console.log(
|
|
67
|
+
`[Appwrite Cleanup] Skipping tracking table: ${table.name}`
|
|
68
|
+
);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
scanned++;
|
|
72
|
+
try {
|
|
73
|
+
let hasMore = true;
|
|
74
|
+
let cursor;
|
|
75
|
+
while (hasMore) {
|
|
76
|
+
const queries = [
|
|
77
|
+
Query.greaterThanEqual("$createdAt", testStartTime),
|
|
78
|
+
Query.limit(100)
|
|
79
|
+
];
|
|
80
|
+
if (cursor) {
|
|
81
|
+
queries.push(Query.cursorAfter(cursor));
|
|
82
|
+
}
|
|
83
|
+
const rows = await tablesDB.listRows({
|
|
84
|
+
databaseId: db.$id,
|
|
85
|
+
tableId: table.$id,
|
|
86
|
+
queries
|
|
87
|
+
});
|
|
88
|
+
console.log(
|
|
89
|
+
`[Appwrite Cleanup] Table "${table.name}": found ${rows.rows.length} rows created after test start`
|
|
90
|
+
);
|
|
91
|
+
for (const row of rows.rows) {
|
|
92
|
+
const rowJson = JSON.stringify(row);
|
|
93
|
+
const shouldDelete = userId && rowJson.includes(userId);
|
|
94
|
+
if (shouldDelete) {
|
|
95
|
+
try {
|
|
96
|
+
await tablesDB.deleteRow({
|
|
97
|
+
databaseId: db.$id,
|
|
98
|
+
tableId: table.$id,
|
|
99
|
+
rowId: row.$id
|
|
100
|
+
});
|
|
101
|
+
deleted.push(`row:${db.$id}/${table.$id}/${row.$id}`);
|
|
102
|
+
console.log(
|
|
103
|
+
`[Appwrite Cleanup] Deleted row ${row.$id} from ${table.name}`
|
|
104
|
+
);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
failed.push(`row:${db.$id}/${table.$id}/${row.$id}`);
|
|
107
|
+
console.warn(
|
|
108
|
+
`[Appwrite Cleanup] Failed to delete row ${row.$id}:`,
|
|
109
|
+
error
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (rows.rows.length < 100) {
|
|
115
|
+
hasMore = false;
|
|
116
|
+
} else {
|
|
117
|
+
cursor = rows.rows[rows.rows.length - 1].$id;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.warn(
|
|
122
|
+
`[Appwrite Cleanup] Error scanning table ${table.name}:`,
|
|
123
|
+
error
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
console.log("[Appwrite Cleanup] Scanning storage buckets...");
|
|
129
|
+
const buckets = await storage.listBuckets();
|
|
130
|
+
console.log(
|
|
131
|
+
`[Appwrite Cleanup] Found ${buckets.buckets.length} buckets to scan`
|
|
132
|
+
);
|
|
133
|
+
for (const bucket of buckets.buckets) {
|
|
134
|
+
scanned++;
|
|
135
|
+
try {
|
|
136
|
+
let hasMore = true;
|
|
137
|
+
let cursor;
|
|
138
|
+
while (hasMore) {
|
|
139
|
+
const queries = [
|
|
140
|
+
Query.greaterThanEqual("$createdAt", testStartTime),
|
|
141
|
+
Query.limit(100)
|
|
142
|
+
];
|
|
143
|
+
if (cursor) {
|
|
144
|
+
queries.push(Query.cursorAfter(cursor));
|
|
145
|
+
}
|
|
146
|
+
const files = await storage.listFiles({
|
|
147
|
+
bucketId: bucket.$id,
|
|
148
|
+
queries
|
|
149
|
+
});
|
|
150
|
+
console.log(
|
|
151
|
+
`[Appwrite Cleanup] Bucket "${bucket.name}": found ${files.files.length} files created after test start`
|
|
152
|
+
);
|
|
153
|
+
for (const file of files.files) {
|
|
154
|
+
const fileRecord = file;
|
|
155
|
+
const createdBy = fileRecord.$createdBy;
|
|
156
|
+
const shouldDelete = userId && (createdBy === userId || file.name.includes(userId));
|
|
157
|
+
if (shouldDelete) {
|
|
158
|
+
try {
|
|
159
|
+
await storage.deleteFile({
|
|
160
|
+
bucketId: bucket.$id,
|
|
161
|
+
fileId: file.$id
|
|
162
|
+
});
|
|
163
|
+
deleted.push(`file:${bucket.$id}/${file.$id}`);
|
|
164
|
+
console.log(
|
|
165
|
+
`[Appwrite Cleanup] Deleted file ${file.$id} from bucket ${bucket.name}`
|
|
166
|
+
);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
failed.push(`file:${bucket.$id}/${file.$id}`);
|
|
169
|
+
console.warn(
|
|
170
|
+
`[Appwrite Cleanup] Failed to delete file ${file.$id}:`,
|
|
171
|
+
error
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (files.files.length < 100) {
|
|
177
|
+
hasMore = false;
|
|
178
|
+
} else {
|
|
179
|
+
cursor = files.files[files.files.length - 1].$id;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.warn(
|
|
184
|
+
`[Appwrite Cleanup] Error scanning bucket ${bucket.name}:`,
|
|
185
|
+
error
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (userId) {
|
|
190
|
+
console.log(`[Appwrite Cleanup] Deleting test user: ${userId}`);
|
|
191
|
+
try {
|
|
192
|
+
await users.delete(userId);
|
|
193
|
+
deleted.push(`user:${userId}`);
|
|
194
|
+
console.log(`[Appwrite Cleanup] Deleted user ${userId}`);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
failed.push(`user:${userId}`);
|
|
197
|
+
console.warn(
|
|
198
|
+
`[Appwrite Cleanup] Failed to delete user ${userId}:`,
|
|
199
|
+
error
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error("[Appwrite Cleanup] Error during cleanup scan:", error);
|
|
205
|
+
}
|
|
206
|
+
console.log(
|
|
207
|
+
`[Appwrite Cleanup] Cleanup complete. Scanned: ${scanned}, Deleted: ${deleted.length}, Failed: ${failed.length}`
|
|
208
|
+
);
|
|
209
|
+
return {
|
|
210
|
+
success: failed.length === 0,
|
|
211
|
+
scanned,
|
|
212
|
+
deleted,
|
|
213
|
+
failed
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
name: "appwrite",
|
|
218
|
+
async configure() {
|
|
219
|
+
},
|
|
220
|
+
methods,
|
|
221
|
+
cleanupUntracked
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
var appwriteTypeMappings = {
|
|
225
|
+
row: "appwrite.deleteRow",
|
|
226
|
+
file: "appwrite.deleteFile",
|
|
227
|
+
team: "appwrite.deleteTeam",
|
|
228
|
+
user: "appwrite.deleteUser",
|
|
229
|
+
membership: "appwrite.deleteMembership"
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export { appwriteTypeMappings, createAppwriteProvider };
|
|
233
|
+
//# sourceMappingURL=chunk-4B54JUOP.js.map
|
|
234
|
+
//# sourceMappingURL=chunk-4B54JUOP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/providers/appwrite/index.ts"],"names":[],"mappings":"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","file":"chunk-4B54JUOP.js","sourcesContent":["import { Client, Users, TablesDB, Storage, Teams, Query } from 'node-appwrite';\nimport type {\n CleanupProvider,\n CleanupHandler,\n CleanupUntrackedOptions,\n CleanupUntrackedResult,\n} from '../../core/cleanup/types.js';\nimport type { TrackedResource } from '../../integration/index.js';\n\ninterface AppwriteConfig {\n endpoint: string;\n projectId: string;\n apiKey: string;\n}\n\nexport function createAppwriteProvider(config: AppwriteConfig): CleanupProvider {\n const client = new Client()\n .setEndpoint(config.endpoint)\n .setProject(config.projectId)\n .setKey(config.apiKey);\n\n const tablesDB = new TablesDB(client);\n const storage = new Storage(client);\n const teams = new Teams(client);\n const users = new Users(client);\n\n const methods: Record<string, CleanupHandler> = {\n deleteRow: async (resource: TrackedResource) => {\n const databaseId = resource.databaseId as string;\n const tableId = resource.tableId as string;\n\n if (!databaseId || !tableId) {\n throw new Error(`Missing databaseId or tableId for row ${resource.id}`);\n }\n\n await tablesDB.deleteRow({\n databaseId,\n tableId,\n rowId: resource.id,\n });\n },\n\n deleteFile: async (resource: TrackedResource) => {\n const bucketId = resource.bucketId as string;\n\n if (!bucketId) {\n throw new Error(`Missing bucketId for file ${resource.id}`);\n }\n\n await storage.deleteFile(bucketId, resource.id);\n },\n\n deleteTeam: async (resource: TrackedResource) => {\n await teams.delete(resource.id);\n },\n\n deleteUser: async (resource: TrackedResource) => {\n await users.delete(resource.id);\n },\n\n deleteMembership: async (resource: TrackedResource) => {\n const teamId = resource.teamId as string;\n\n if (!teamId) {\n throw new Error(`Missing teamId for membership ${resource.id}`);\n }\n\n await teams.deleteMembership(teamId, resource.id);\n },\n };\n\n /**\n * Scan all Appwrite tables for resources created after testStartTime\n * that contain the userId in any field, and delete them.\n */\n async function cleanupUntracked(\n options: CleanupUntrackedOptions\n ): Promise<CleanupUntrackedResult> {\n const { testStartTime, userId, sessionId } = options;\n const deleted: string[] = [];\n const failed: string[] = [];\n let scanned = 0;\n\n console.log(\n `[Appwrite Cleanup] Starting untracked cleanup for session ${sessionId || 'unknown'}`\n );\n console.log(`[Appwrite Cleanup] Test start time: ${testStartTime}`);\n console.log(`[Appwrite Cleanup] User ID to match: ${userId || 'none'}`);\n\n try {\n // 1. List all databases\n const databases = await tablesDB.list();\n console.log(\n `[Appwrite Cleanup] Found ${databases.databases.length} databases to scan`\n );\n\n for (const db of databases.databases) {\n // 2. List all tables in each database\n const tables = await tablesDB.listTables({ databaseId: db.$id });\n console.log(\n `[Appwrite Cleanup] Database \"${db.name}\" (${db.$id}): ${tables.tables.length} tables`\n );\n\n for (const table of tables.tables) {\n // Skip tracking tables (tables starting with _intellitester)\n if (table.name.startsWith('_intellitester')) {\n console.log(\n `[Appwrite Cleanup] Skipping tracking table: ${table.name}`\n );\n continue;\n }\n\n scanned++;\n\n try {\n // 3. Query for rows created after testStartTime with pagination\n let hasMore = true;\n let cursor: string | undefined;\n\n while (hasMore) {\n const queries = [\n Query.greaterThanEqual('$createdAt', testStartTime),\n Query.limit(100),\n ];\n\n if (cursor) {\n queries.push(Query.cursorAfter(cursor));\n }\n\n const rows = await tablesDB.listRows({\n databaseId: db.$id,\n tableId: table.$id,\n queries,\n });\n\n console.log(\n `[Appwrite Cleanup] Table \"${table.name}\": found ${rows.rows.length} rows created after test start`\n );\n\n for (const row of rows.rows) {\n // 4. Check if any field contains userId\n const rowJson = JSON.stringify(row);\n const shouldDelete = userId && rowJson.includes(userId);\n\n if (shouldDelete) {\n try {\n await tablesDB.deleteRow({\n databaseId: db.$id,\n tableId: table.$id,\n rowId: row.$id,\n });\n deleted.push(`row:${db.$id}/${table.$id}/${row.$id}`);\n console.log(\n `[Appwrite Cleanup] Deleted row ${row.$id} from ${table.name}`\n );\n } catch (error) {\n failed.push(`row:${db.$id}/${table.$id}/${row.$id}`);\n console.warn(\n `[Appwrite Cleanup] Failed to delete row ${row.$id}:`,\n error\n );\n }\n }\n }\n\n // Check if we need to paginate\n if (rows.rows.length < 100) {\n hasMore = false;\n } else {\n cursor = rows.rows[rows.rows.length - 1].$id;\n }\n }\n } catch (error) {\n console.warn(\n `[Appwrite Cleanup] Error scanning table ${table.name}:`,\n error\n );\n }\n }\n }\n\n // 5. Scan storage buckets for files\n console.log('[Appwrite Cleanup] Scanning storage buckets...');\n const buckets = await storage.listBuckets();\n console.log(\n `[Appwrite Cleanup] Found ${buckets.buckets.length} buckets to scan`\n );\n\n for (const bucket of buckets.buckets) {\n scanned++;\n\n try {\n let hasMore = true;\n let cursor: string | undefined;\n\n while (hasMore) {\n const queries = [\n Query.greaterThanEqual('$createdAt', testStartTime),\n Query.limit(100),\n ];\n\n if (cursor) {\n queries.push(Query.cursorAfter(cursor));\n }\n\n const files = await storage.listFiles({\n bucketId: bucket.$id,\n queries,\n });\n\n console.log(\n `[Appwrite Cleanup] Bucket \"${bucket.name}\": found ${files.files.length} files created after test start`\n );\n\n for (const file of files.files) {\n // Files don't have custom fields, but check name patterns\n // Note: $createdBy might not exist on all file objects\n const fileRecord = file as Record<string, unknown>;\n const createdBy = fileRecord.$createdBy as string | undefined;\n const shouldDelete =\n userId &&\n (createdBy === userId || file.name.includes(userId));\n\n if (shouldDelete) {\n try {\n await storage.deleteFile({\n bucketId: bucket.$id,\n fileId: file.$id,\n });\n deleted.push(`file:${bucket.$id}/${file.$id}`);\n console.log(\n `[Appwrite Cleanup] Deleted file ${file.$id} from bucket ${bucket.name}`\n );\n } catch (error) {\n failed.push(`file:${bucket.$id}/${file.$id}`);\n console.warn(\n `[Appwrite Cleanup] Failed to delete file ${file.$id}:`,\n error\n );\n }\n }\n }\n\n // Check if we need to paginate\n if (files.files.length < 100) {\n hasMore = false;\n } else {\n cursor = files.files[files.files.length - 1].$id;\n }\n }\n } catch (error) {\n console.warn(\n `[Appwrite Cleanup] Error scanning bucket ${bucket.name}:`,\n error\n );\n }\n }\n\n // 6. Delete the test user last\n if (userId) {\n console.log(`[Appwrite Cleanup] Deleting test user: ${userId}`);\n try {\n await users.delete(userId);\n deleted.push(`user:${userId}`);\n console.log(`[Appwrite Cleanup] Deleted user ${userId}`);\n } catch (error) {\n failed.push(`user:${userId}`);\n console.warn(\n `[Appwrite Cleanup] Failed to delete user ${userId}:`,\n error\n );\n }\n }\n } catch (error) {\n console.error('[Appwrite Cleanup] Error during cleanup scan:', error);\n }\n\n console.log(\n `[Appwrite Cleanup] Cleanup complete. Scanned: ${scanned}, Deleted: ${deleted.length}, Failed: ${failed.length}`\n );\n\n return {\n success: failed.length === 0,\n scanned,\n deleted,\n failed,\n };\n }\n\n return {\n name: 'appwrite',\n async configure() {\n // Client is already configured in the factory function\n // This is called by the cleanup executor but we don't need to do anything\n },\n methods,\n cleanupUntracked,\n };\n}\n\n// Default type mappings for Appwrite resources\nexport const appwriteTypeMappings: Record<string, string> = {\n row: 'appwrite.deleteRow',\n file: 'appwrite.deleteFile',\n team: 'appwrite.deleteTeam',\n user: 'appwrite.deleteUser',\n membership: 'appwrite.deleteMembership',\n};\n"]}
|