@adobe/aio-cli-plugin-app-storage 1.2.0 → 1.3.0-pre.2026-02-20.sha-df98265c
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 +722 -1
- package/package.json +25 -5
- package/src/BaseCommand.js +11 -58
- package/src/DBBaseCommand.js +112 -0
- package/src/StateBaseCommand.js +87 -0
- package/src/commands/app/add/db.js +20 -0
- package/src/commands/app/db/collection/create.js +119 -0
- package/src/commands/app/db/collection/drop.js +90 -0
- package/src/commands/app/db/collection/list.js +100 -0
- package/src/commands/app/db/collection/rename.js +98 -0
- package/src/commands/app/db/collection/stats.js +94 -0
- package/src/commands/app/db/delete.js +89 -0
- package/src/commands/app/db/document/count.js +96 -0
- package/src/commands/app/db/document/delete.js +95 -0
- package/src/commands/app/db/document/find.js +133 -0
- package/src/commands/app/db/document/insert.js +147 -0
- package/src/commands/app/db/document/replace.js +122 -0
- package/src/commands/app/db/document/update.js +144 -0
- package/src/commands/app/db/index/create.js +170 -0
- package/src/commands/app/db/index/drop.js +87 -0
- package/src/commands/app/db/index/list.js +82 -0
- package/src/commands/app/db/org/stats.js +111 -0
- package/src/commands/app/db/ping.js +77 -0
- package/src/commands/app/db/provision.js +190 -0
- package/src/commands/app/db/stats.js +101 -0
- package/src/commands/app/db/status.js +159 -0
- package/src/commands/app/state/delete.js +3 -3
- package/src/commands/app/state/get.js +2 -2
- package/src/commands/app/state/list.js +3 -3
- package/src/commands/app/state/put.js +4 -4
- package/src/commands/app/state/stats.js +2 -2
- package/src/constants/db.js +32 -0
- package/src/constants/global.js +14 -0
- package/src/{constants.js → constants/state.js} +3 -0
- package/src/utils/inputValidation.js +74 -0
- package/src/utils/output.js +35 -0
- package/oclif.manifest.json +0 -316
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
|
|
7
|
+
Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { DBBaseCommand } from '../../../../DBBaseCommand.js'
|
|
14
|
+
import { Args, Flags } from '@oclif/core'
|
|
15
|
+
import chalk from 'chalk'
|
|
16
|
+
import { isNonEmptyString } from '../../../../utils/inputValidation.js'
|
|
17
|
+
import { prettyJson } from '../../../../utils/output.js'
|
|
18
|
+
|
|
19
|
+
export class Insert extends DBBaseCommand {
|
|
20
|
+
async run () {
|
|
21
|
+
const { collection, documents } = this.args
|
|
22
|
+
const { bypassDocumentValidation } = this.flags
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
this.log(chalk.blue(`Inserting ${documents.length} documents into collection '${collection}'...`))
|
|
26
|
+
|
|
27
|
+
// Build options object
|
|
28
|
+
const insertOptions = {}
|
|
29
|
+
if (bypassDocumentValidation) {
|
|
30
|
+
insertOptions.bypassDocumentValidation = true
|
|
31
|
+
this.log(chalk.dim(' Bypassing document validation'))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const client = await this.db.connect()
|
|
35
|
+
const coll = await client.collection(collection)
|
|
36
|
+
|
|
37
|
+
// Perform the insert operation
|
|
38
|
+
const result = await coll.insertMany(documents, insertOptions)
|
|
39
|
+
|
|
40
|
+
this.debugLogger?.info?.('Documents inserted successfully:', result)
|
|
41
|
+
|
|
42
|
+
const response = {
|
|
43
|
+
collection,
|
|
44
|
+
status: 'inserted',
|
|
45
|
+
namespace: this.rtNamespace,
|
|
46
|
+
timestamp: new Date().toISOString(),
|
|
47
|
+
result
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (bypassDocumentValidation) {
|
|
51
|
+
response.options = insertOptions
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.log(chalk.green(`Successfully inserted ${result.insertedCount} documents into collection '${collection}'`))
|
|
55
|
+
this.log(chalk.dim(` Collection: ${collection}`))
|
|
56
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
57
|
+
|
|
58
|
+
if (result.insertedIds && Object.keys(result.insertedIds).length > 0) {
|
|
59
|
+
this.log(chalk.dim(` Inserted IDs: ${JSON.stringify(result.insertedIds)}`))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.log(chalk.dim(` Details:\n${prettyJson(result)}`))
|
|
63
|
+
|
|
64
|
+
this.log(chalk.dim(` Inserted: ${new Date().toLocaleString()}`))
|
|
65
|
+
|
|
66
|
+
return response
|
|
67
|
+
} catch (error) {
|
|
68
|
+
this.debugLogger?.error?.('Error inserting documents:', error)
|
|
69
|
+
|
|
70
|
+
const errorMessage = `Failed to insert documents into collection '${collection}': ${error.message}`
|
|
71
|
+
|
|
72
|
+
this.log(chalk.red('Failed to insert documents'))
|
|
73
|
+
this.log(chalk.dim(` Collection: ${collection}`))
|
|
74
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
75
|
+
this.log(chalk.dim(` Error: ${error.message}`))
|
|
76
|
+
|
|
77
|
+
this.error(errorMessage)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Insert.description = 'Insert one or more documents into a collection'
|
|
83
|
+
|
|
84
|
+
Insert.examples = [
|
|
85
|
+
'$ aio app db document insert users \'{"name": "John", "age": 30}\'',
|
|
86
|
+
'$ aio app db document insert products \'[{"id": 1, "name": "Product A"}, {"id": 2, "name": "Product B"}]\' --json',
|
|
87
|
+
'$ aio app db document insert temp \'{"data": "test"}\' --bypassDocumentValidation',
|
|
88
|
+
'$ aio app db doc insert bulk \'[{"field": "foo"}, {"field": "bar"}]\' --bypassDocumentValidation --json'
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
Insert.args = {
|
|
92
|
+
collection: Args.string({
|
|
93
|
+
name: 'collection',
|
|
94
|
+
description: 'The name of the collection to insert documents into',
|
|
95
|
+
required: true,
|
|
96
|
+
parse: input => isNonEmptyString(input, 'Collection name')
|
|
97
|
+
}),
|
|
98
|
+
documents: Args.string({
|
|
99
|
+
name: 'documents',
|
|
100
|
+
description: 'JSON object or array of documents to insert',
|
|
101
|
+
required: true,
|
|
102
|
+
parse: input => {
|
|
103
|
+
if (typeof input !== 'string' || input.trim().length === 0) {
|
|
104
|
+
throw new Error('Documents: Must be a JSON string representing an object or non-empty array')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let result
|
|
108
|
+
try {
|
|
109
|
+
result = JSON.parse(input)
|
|
110
|
+
} catch (e) {
|
|
111
|
+
throw new Error(`Documents: JSON parse error: ${e.message}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const isSingleDoc = !Array.isArray(result)
|
|
115
|
+
if (isSingleDoc) {
|
|
116
|
+
result = [result]
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (result.length === 0) {
|
|
120
|
+
throw new Error('Documents: Cannot be empty')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Validate each document is an object
|
|
124
|
+
for (let i = 0; i < result.length; i++) {
|
|
125
|
+
if (typeof result[i] !== 'object' || result[i] === null || Array.isArray(result[i])) {
|
|
126
|
+
if (isSingleDoc) {
|
|
127
|
+
throw new Error('Documents: Must be a JSON string representing an object or non-empty array')
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`Documents: Element at index ${i} must be an object`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
Insert.flags = {
|
|
139
|
+
...DBBaseCommand.flags,
|
|
140
|
+
bypassDocumentValidation: Flags.boolean({
|
|
141
|
+
char: 'b',
|
|
142
|
+
description: 'Bypass schema validation if present',
|
|
143
|
+
default: false
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Insert.aliases = ['app:db:doc:insert']
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
|
|
7
|
+
Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { DBBaseCommand } from '../../../../DBBaseCommand.js'
|
|
14
|
+
import { Args, Flags } from '@oclif/core'
|
|
15
|
+
import chalk from 'chalk'
|
|
16
|
+
import { asObject, isNonEmptyString } from '../../../../utils/inputValidation.js'
|
|
17
|
+
|
|
18
|
+
export class Replace extends DBBaseCommand {
|
|
19
|
+
async run () {
|
|
20
|
+
const { collection, filter, replacement } = this.args
|
|
21
|
+
const { upsert } = this.flags
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
this.log(chalk.blue(`Replacing document in collection '${collection}'...`))
|
|
25
|
+
|
|
26
|
+
if (upsert) {
|
|
27
|
+
this.log(chalk.dim(' Upsert enabled: Will create document if not found'))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const client = await this.db.connect()
|
|
31
|
+
const coll = client.collection(collection)
|
|
32
|
+
|
|
33
|
+
// Build options
|
|
34
|
+
const options = {}
|
|
35
|
+
if (upsert) {
|
|
36
|
+
options.upsert = true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Replace the document
|
|
40
|
+
const result = await coll.replaceOne(filter, replacement, options)
|
|
41
|
+
|
|
42
|
+
this.debugLogger?.info?.('Document replaced successfully:', result)
|
|
43
|
+
|
|
44
|
+
const response = {
|
|
45
|
+
collection,
|
|
46
|
+
filter,
|
|
47
|
+
replacement,
|
|
48
|
+
namespace: this.rtNamespace,
|
|
49
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
result
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (result.matchedCount > 0) {
|
|
54
|
+
this.log(chalk.green(`Document replaced successfully in collection '${collection}'`))
|
|
55
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
56
|
+
} else if (upsert && result.upsertedId) {
|
|
57
|
+
this.log(chalk.green(`Document created (upserted) in collection '${collection}'`))
|
|
58
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
59
|
+
this.log(chalk.dim(` Upserted ID: ${result.upsertedId}`))
|
|
60
|
+
this.log(chalk.dim(` Upserted count: ${result.upsertedCount}`))
|
|
61
|
+
} else {
|
|
62
|
+
this.log(chalk.yellow(`No document found in collection '${collection}' matching the filter`))
|
|
63
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.log(chalk.dim(` Replaced: ${new Date().toLocaleString()}`))
|
|
67
|
+
|
|
68
|
+
return response
|
|
69
|
+
} catch (error) {
|
|
70
|
+
this.debugLogger?.error?.('Error replacing document:', error)
|
|
71
|
+
|
|
72
|
+
const errorMessage = `Failed to replace document in collection '${collection}': ${error.message}`
|
|
73
|
+
|
|
74
|
+
this.log(chalk.red('Failed to replace document'))
|
|
75
|
+
this.log(chalk.dim(` Collection: ${collection}`))
|
|
76
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
77
|
+
|
|
78
|
+
this.error(errorMessage)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Replace.description = 'Replace a single document in a collection'
|
|
84
|
+
|
|
85
|
+
Replace.examples = [
|
|
86
|
+
'$ aio app db document replace users \'{"name": "John"}\' \'{"name": "John Doe", "age": 30, "status": "active"}\'',
|
|
87
|
+
'$ aio app db document replace products \'{"id": "123"}\' \'{"id": "123", "name": "New Product", "price": 99.99}\' --json',
|
|
88
|
+
'$ aio app db document replace posts \'{"slug": "hello-world"}\' \'{"title": "Hello World", "content": "Updated content", "status": "published"}\' --upsert',
|
|
89
|
+
'$ aio app db doc replace users \'{"email": "john@example.com"}\' \'{"email": "john@example.com", "name": "John", "verified": true}\' --upsert --json'
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
Replace.args = {
|
|
93
|
+
collection: Args.string({
|
|
94
|
+
name: 'collection',
|
|
95
|
+
description: 'The name of the collection',
|
|
96
|
+
required: true,
|
|
97
|
+
parse: input => isNonEmptyString(input, 'Collection name')
|
|
98
|
+
}),
|
|
99
|
+
filter: Args.string({
|
|
100
|
+
name: 'filter',
|
|
101
|
+
description: 'The filter document (JSON string)',
|
|
102
|
+
required: true,
|
|
103
|
+
parse: input => asObject(input, 'Filter')
|
|
104
|
+
}),
|
|
105
|
+
replacement: Args.string({
|
|
106
|
+
name: 'replacement',
|
|
107
|
+
description: 'The replacement document (JSON string)',
|
|
108
|
+
required: true,
|
|
109
|
+
parse: input => asObject(input, 'Replacement')
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Replace.flags = {
|
|
114
|
+
...DBBaseCommand.flags,
|
|
115
|
+
upsert: Flags.boolean({
|
|
116
|
+
char: 'u',
|
|
117
|
+
description: 'If no document is found, create a new one',
|
|
118
|
+
default: false
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
Replace.aliases = ['app:db:doc:replace']
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
|
|
7
|
+
Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { DBBaseCommand } from '../../../../DBBaseCommand.js'
|
|
14
|
+
import { Args, Flags } from '@oclif/core'
|
|
15
|
+
import chalk from 'chalk'
|
|
16
|
+
import { asObject, isNonEmptyString } from '../../../../utils/inputValidation.js'
|
|
17
|
+
|
|
18
|
+
export class Update extends DBBaseCommand {
|
|
19
|
+
async run () {
|
|
20
|
+
const { collection, filter, update } = this.args
|
|
21
|
+
const { many, upsert } = this.flags
|
|
22
|
+
const docPlural = many ? '(s)' : ''
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
this.log(chalk.blue(`Updating document${docPlural} in collection '${collection}'...`))
|
|
26
|
+
this.log(chalk.dim(` Filter: ${JSON.stringify(filter)}`))
|
|
27
|
+
this.log(chalk.dim(` Update: ${JSON.stringify(update)}`))
|
|
28
|
+
|
|
29
|
+
if (upsert) {
|
|
30
|
+
this.log(chalk.dim(' Upsert enabled: Will create document if not found'))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const client = await this.db.connect()
|
|
34
|
+
const coll = client.collection(collection)
|
|
35
|
+
|
|
36
|
+
// Build options
|
|
37
|
+
const options = {}
|
|
38
|
+
if (upsert) {
|
|
39
|
+
options.upsert = true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Update the document
|
|
43
|
+
const result = await (many ? coll.updateMany(filter, update, options) : coll.updateOne(filter, update, options))
|
|
44
|
+
|
|
45
|
+
this.debugLogger?.info?.(`Document${docPlural} updated successfully:`, result)
|
|
46
|
+
|
|
47
|
+
const response = {
|
|
48
|
+
collection,
|
|
49
|
+
filter,
|
|
50
|
+
update,
|
|
51
|
+
namespace: this.rtNamespace,
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
result
|
|
54
|
+
}
|
|
55
|
+
const optionOutput = { ...options }
|
|
56
|
+
if (many) {
|
|
57
|
+
optionOutput.many = true
|
|
58
|
+
}
|
|
59
|
+
if (Object.keys(optionOutput).length > 0) {
|
|
60
|
+
response.options = optionOutput
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (result.matchedCount > 0) {
|
|
64
|
+
if (result.modifiedCount > 0) {
|
|
65
|
+
this.log(chalk.green(`Document${docPlural} updated successfully in collection '${collection}'`))
|
|
66
|
+
this.log(chalk.dim(` Collection: ${collection}`))
|
|
67
|
+
this.log(chalk.dim(` Matched Count: ${result.matchedCount}`))
|
|
68
|
+
this.log(chalk.dim(` Modified Count: ${result.modifiedCount}`))
|
|
69
|
+
} else {
|
|
70
|
+
this.log(chalk.green(`${result.matchedCount} matching document${docPlural} found in collection '${collection}', but no update was necessary`))
|
|
71
|
+
}
|
|
72
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
73
|
+
} else if (result.upsertedId) {
|
|
74
|
+
this.log(chalk.green(`Document created (upserted) in collection '${collection}'`))
|
|
75
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
76
|
+
this.log(chalk.dim(` Upserted ID: ${result.upsertedId}`))
|
|
77
|
+
this.log(chalk.dim(` Upserted count: ${result.upsertedCount}`))
|
|
78
|
+
} else {
|
|
79
|
+
this.log(chalk.yellow(`No document found in collection '${collection}' matching the filter`))
|
|
80
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this.log(chalk.dim(` Updated: ${new Date().toLocaleString()}`))
|
|
84
|
+
|
|
85
|
+
return response
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.debugLogger?.error?.(`Error updating document${docPlural}:`, error)
|
|
88
|
+
|
|
89
|
+
const errorMessage = `Failed to update document${docPlural} in collection '${collection}': ${error.message}`
|
|
90
|
+
|
|
91
|
+
this.log(chalk.red(`Failed to update document${docPlural}`))
|
|
92
|
+
this.log(chalk.dim(` Collection: ${collection}`))
|
|
93
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
94
|
+
|
|
95
|
+
this.error(errorMessage)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Update.description = 'Update document(s) in a collection'
|
|
101
|
+
|
|
102
|
+
Update.examples = [
|
|
103
|
+
'$ aio app db document update users \'{"name": "John"}\' \'{"$set": {"age": 31}}\'',
|
|
104
|
+
'$ aio app db document update products \'{"id": "123"}\' \'{"$inc": {"stock": -1}}\' --json',
|
|
105
|
+
'$ aio app db document update posts \'{"slug": "hello-world"}\' \'{"$set": {"status": "published"}}\' --many',
|
|
106
|
+
'$ aio app db doc update users \'{"email": "john@example.com"}\' \'{"$set": {"lastLogin": "2024-01-01"}}\' --upsert'
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
Update.args = {
|
|
110
|
+
collection: Args.string({
|
|
111
|
+
name: 'collection',
|
|
112
|
+
description: 'The name of the collection',
|
|
113
|
+
required: true,
|
|
114
|
+
parse: input => isNonEmptyString(input, 'Collection name')
|
|
115
|
+
}),
|
|
116
|
+
filter: Args.string({
|
|
117
|
+
name: 'filter',
|
|
118
|
+
description: 'The filter document (JSON string)',
|
|
119
|
+
required: true,
|
|
120
|
+
parse: input => asObject(input, 'Filter')
|
|
121
|
+
}),
|
|
122
|
+
update: Args.string({
|
|
123
|
+
name: 'update',
|
|
124
|
+
description: 'The update document (JSON string)',
|
|
125
|
+
required: true,
|
|
126
|
+
parse: input => asObject(input, 'Update')
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
Update.flags = {
|
|
131
|
+
...DBBaseCommand.flags,
|
|
132
|
+
upsert: Flags.boolean({
|
|
133
|
+
char: 'u',
|
|
134
|
+
description: 'If no document is found, create a new one',
|
|
135
|
+
default: false
|
|
136
|
+
}),
|
|
137
|
+
many: Flags.boolean({
|
|
138
|
+
char: 'm',
|
|
139
|
+
description: 'Update all documents matching the filter. Without this option, only the first matching document is updated.',
|
|
140
|
+
default: false
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
Update.aliases = ['app:db:doc:update']
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
|
|
7
|
+
Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { DBBaseCommand } from '../../../../DBBaseCommand.js'
|
|
14
|
+
import { Args, Flags } from '@oclif/core'
|
|
15
|
+
import chalk from 'chalk'
|
|
16
|
+
import { asObject, isNonEmptyString } from '../../../../utils/inputValidation.js'
|
|
17
|
+
import { prettyJson } from '../../../../utils/output.js'
|
|
18
|
+
|
|
19
|
+
// Regular expression to match the -s/--spec or -k/--key flags in <flag>=<value> format
|
|
20
|
+
// if match.groups.spec is defined, it means the flag was -s or --spec
|
|
21
|
+
// if match.groups.key is defined, it means the flag was -k or --key
|
|
22
|
+
const specFlagMatch = /^((?<spec>-s|--spec)|(?<key>-k|--key))=(?<val>.+)/
|
|
23
|
+
|
|
24
|
+
export class Create extends DBBaseCommand {
|
|
25
|
+
getOrderedSpecs () {
|
|
26
|
+
// Key/spec order matters when creating an index and both key and spec can be specified,
|
|
27
|
+
// so we need to parse argv to obtain the proper order since using this.flags loses the order between the two
|
|
28
|
+
const args = this.argv.slice(1) // Ignore the first element (collection name)
|
|
29
|
+
const fullSpec = []
|
|
30
|
+
let specOption = false
|
|
31
|
+
args.forEach((arg) => {
|
|
32
|
+
if (specOption === 'key') {
|
|
33
|
+
// Previous arg was -k or --key
|
|
34
|
+
fullSpec.push(arg)
|
|
35
|
+
specOption = false
|
|
36
|
+
} else if (specOption === 'spec') {
|
|
37
|
+
// Previous arg was -s or --spec
|
|
38
|
+
fullSpec.push(asObject(arg))
|
|
39
|
+
specOption = false
|
|
40
|
+
} else if (arg === '-k' || arg === '--key') {
|
|
41
|
+
// Next arg is a key
|
|
42
|
+
specOption = 'key'
|
|
43
|
+
} else if (arg === '-s' || arg === '--spec') {
|
|
44
|
+
// Next arg is a spec
|
|
45
|
+
specOption = 'spec'
|
|
46
|
+
} else {
|
|
47
|
+
// Check if the arg is in the form of -s=<val>/--spec=<val> or -k=<val>/--key=<val>
|
|
48
|
+
const specMatch = arg.match(specFlagMatch)
|
|
49
|
+
if (specMatch?.groups?.spec) {
|
|
50
|
+
// Spec is a JSON object
|
|
51
|
+
fullSpec.push(asObject(specMatch.groups.val))
|
|
52
|
+
} else if (specMatch?.groups?.key) {
|
|
53
|
+
// Key is a string
|
|
54
|
+
fullSpec.push(specMatch.groups.val)
|
|
55
|
+
}
|
|
56
|
+
specOption = false
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return fullSpec
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async run () {
|
|
64
|
+
const { collection } = this.args
|
|
65
|
+
const { name, unique } = this.flags
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const fullSpec = this.getOrderedSpecs()
|
|
69
|
+
const prettySpec = prettyJson(fullSpec)
|
|
70
|
+
|
|
71
|
+
this.log(chalk.blue(`Creating index ${name ? `'${name}' ` : ''}on collection '${collection}'...`))
|
|
72
|
+
this.log(chalk.dim(` Specification:\n${prettySpec}`))
|
|
73
|
+
if (unique) this.log(chalk.dim(` Unique index: ${unique}`))
|
|
74
|
+
|
|
75
|
+
const client = await this.db.connect()
|
|
76
|
+
const coll = await client.collection(collection)
|
|
77
|
+
|
|
78
|
+
// Build options
|
|
79
|
+
const options = {}
|
|
80
|
+
if (name) options.name = name
|
|
81
|
+
if (unique) options.unique = unique
|
|
82
|
+
|
|
83
|
+
const result = await coll.createIndex(fullSpec, options)
|
|
84
|
+
|
|
85
|
+
this.debugLogger?.info?.('Index created successfully:', result)
|
|
86
|
+
|
|
87
|
+
const response = {
|
|
88
|
+
collection,
|
|
89
|
+
indexName: result,
|
|
90
|
+
specification: fullSpec,
|
|
91
|
+
status: 'created',
|
|
92
|
+
namespace: this.rtNamespace,
|
|
93
|
+
timestamp: new Date().toISOString()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (Object.keys(options).length > 0) response.options = options
|
|
97
|
+
|
|
98
|
+
this.log(chalk.green(`Index '${result}' created successfully in the '${collection}' collection`))
|
|
99
|
+
this.log(chalk.dim(` Specification:\n${prettySpec}`))
|
|
100
|
+
if (unique) this.log(chalk.dim(` Unique: ${unique}`))
|
|
101
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
102
|
+
this.log(chalk.dim(` Created: ${new Date().toLocaleString()}`))
|
|
103
|
+
|
|
104
|
+
return response
|
|
105
|
+
} catch (error) {
|
|
106
|
+
this.debugLogger?.error?.('Error creating index:', error)
|
|
107
|
+
|
|
108
|
+
this.log(chalk.red('Failed to create index'))
|
|
109
|
+
this.log(chalk.dim(` Collection: ${collection}`))
|
|
110
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
111
|
+
|
|
112
|
+
this.error(`Failed to create index on collection '${collection}': ${error.message}`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
Create.description = 'Create a new index on a collection in the database'
|
|
118
|
+
|
|
119
|
+
Create.examples = [
|
|
120
|
+
'$ aio app db index create users --spec \'{"name":1, "age":-1}\'',
|
|
121
|
+
'$ aio app db index create users -s \'{"name":1, "age":-1}\' --name "name_age_index"',
|
|
122
|
+
'$ aio app db index create students -s \'{"name":1}\' --key grade --unique',
|
|
123
|
+
'$ aio app db index create reviews -k sku -k rating',
|
|
124
|
+
'$ aio app db index create products -s \'{"name":"text", "category":"text"}\' --json',
|
|
125
|
+
'$ aio app db index create books -s \'{"author":1}\' -k year',
|
|
126
|
+
'$ aio app db idx create orders --spec \'{"customerId":1}\' --spec \'{"orderDate":-1}\' --name "customer_order_index" --unique'
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
Create.args = {
|
|
130
|
+
collection: Args.string({
|
|
131
|
+
name: 'collection',
|
|
132
|
+
description: 'The name of the collection to create the index on',
|
|
133
|
+
required: true,
|
|
134
|
+
parse: input => isNonEmptyString(input, 'Collection name')
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
Create.flags = {
|
|
139
|
+
...DBBaseCommand.flags,
|
|
140
|
+
spec: Flags.string({
|
|
141
|
+
char: 's',
|
|
142
|
+
helpGroup: 'Requires at least one of the index definition',
|
|
143
|
+
description: 'Index specification as a JSON object (e.g., \'{"name":1, "age":-1}\')',
|
|
144
|
+
multiple: true,
|
|
145
|
+
atLeastOne: ['key', 'spec'],
|
|
146
|
+
parse: input => asObject(input, 'Index specification')
|
|
147
|
+
}),
|
|
148
|
+
key: Flags.string({
|
|
149
|
+
char: 'k',
|
|
150
|
+
helpGroup: 'Requires at least one of the index definition',
|
|
151
|
+
description: 'Index key to use with default specification',
|
|
152
|
+
multiple: true,
|
|
153
|
+
atLeastOne: ['key', 'spec'],
|
|
154
|
+
// Note: untrimmed whitespace input is allowed for index keys
|
|
155
|
+
parse: input => isNonEmptyString(input, 'Index key')
|
|
156
|
+
}),
|
|
157
|
+
name: Flags.string({
|
|
158
|
+
char: 'n',
|
|
159
|
+
description: 'A name that uniquely identifies the index',
|
|
160
|
+
// Note: untrimmed whitespace input is allowed for index names
|
|
161
|
+
parse: input => isNonEmptyString(input, 'Index name')
|
|
162
|
+
}),
|
|
163
|
+
unique: Flags.boolean({
|
|
164
|
+
char: 'u',
|
|
165
|
+
description: 'Creates a unique index so that the collection will not accept insertion or update of documents where the index key value matches an existing value in the index',
|
|
166
|
+
default: false
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
Create.aliases = ['app:db:idx:create']
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
|
|
7
|
+
Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { DBBaseCommand } from '../../../../DBBaseCommand.js'
|
|
14
|
+
import { Args } from '@oclif/core'
|
|
15
|
+
import chalk from 'chalk'
|
|
16
|
+
import { isNonEmptyString } from '../../../../utils/inputValidation.js'
|
|
17
|
+
import { prettyJson } from '../../../../utils/output.js'
|
|
18
|
+
|
|
19
|
+
export class Drop extends DBBaseCommand {
|
|
20
|
+
async run () {
|
|
21
|
+
const { collection, indexName } = this.args
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
this.log(chalk.blue(`Dropping index '${indexName}' from collection '${collection}'...`))
|
|
25
|
+
|
|
26
|
+
const client = await this.db.connect()
|
|
27
|
+
const coll = await client.collection(collection)
|
|
28
|
+
|
|
29
|
+
const result = await coll.dropIndex(indexName)
|
|
30
|
+
|
|
31
|
+
this.debugLogger?.info?.('Index dropped successfully:', result)
|
|
32
|
+
|
|
33
|
+
const response = {
|
|
34
|
+
collection,
|
|
35
|
+
indexName,
|
|
36
|
+
status: 'dropped',
|
|
37
|
+
namespace: this.rtNamespace,
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
result
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.log(chalk.green(`Index '${indexName}' dropped successfully`))
|
|
43
|
+
this.log(chalk.dim(` Details:\n${prettyJson(result)}`))
|
|
44
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
45
|
+
this.log(chalk.dim(` Dropped: ${new Date().toLocaleString()}`))
|
|
46
|
+
|
|
47
|
+
return response
|
|
48
|
+
} catch (error) {
|
|
49
|
+
this.debugLogger?.error?.('Error dropping index:', error)
|
|
50
|
+
|
|
51
|
+
this.log(chalk.red('Failed to drop index'))
|
|
52
|
+
this.log(chalk.dim(` Collection: ${collection}`))
|
|
53
|
+
this.log(chalk.dim(` Index: ${indexName}`))
|
|
54
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
55
|
+
this.log(chalk.dim(` Error: ${error.message}`))
|
|
56
|
+
|
|
57
|
+
this.error(`Failed to drop index '${indexName}' from collection '${collection}': ${error.message}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Drop.description = 'Drop an index from a collection in the database'
|
|
63
|
+
|
|
64
|
+
Drop.examples = [
|
|
65
|
+
'$ aio app db index drop users name_age_index',
|
|
66
|
+
'$ aio app db index drop products category_1 --json',
|
|
67
|
+
'$ aio app db idx drop orders orderDate_index'
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
Drop.args = {
|
|
71
|
+
collection: Args.string({
|
|
72
|
+
name: 'collection',
|
|
73
|
+
description: 'The name of the collection to drop the index from',
|
|
74
|
+
required: true,
|
|
75
|
+
parse: input => isNonEmptyString(input, 'Collection name')
|
|
76
|
+
}),
|
|
77
|
+
indexName: Args.string({
|
|
78
|
+
name: 'indexName',
|
|
79
|
+
description: 'The name of the index to drop',
|
|
80
|
+
required: true,
|
|
81
|
+
parse: input => isNonEmptyString(input, 'Index name')
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Drop.flags = DBBaseCommand.flags
|
|
86
|
+
|
|
87
|
+
Drop.aliases = ['app:db:idx:drop']
|