@adobe/aio-cli-plugin-app-storage 1.1.0 → 1.2.0-pre.2025-11-18.sha-9c4079ac
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 +665 -1
- package/package.json +33 -6
- 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/ping.js +77 -0
- package/src/commands/app/db/provision.js +190 -0
- package/src/commands/app/db/stats.js +87 -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 -311
|
@@ -0,0 +1,133 @@
|
|
|
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 { asObject, isNonEmptyString } from '../../../../utils/inputValidation.js'
|
|
16
|
+
import chalk from 'chalk'
|
|
17
|
+
import { prettyJson } from '../../../../utils/output.js'
|
|
18
|
+
|
|
19
|
+
export class Find extends DBBaseCommand {
|
|
20
|
+
async run () {
|
|
21
|
+
const { collection, filter } = this.args
|
|
22
|
+
const { limit, skip, sort, projection } = this.flags
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
this.log(chalk.blue(`Finding documents in collection '${collection}'...`))
|
|
26
|
+
this.log(chalk.dim(` Filter:\n${prettyJson(filter)}`))
|
|
27
|
+
|
|
28
|
+
// Prepare options for find
|
|
29
|
+
const options = { limit }
|
|
30
|
+
this.log(chalk.dim(` Limit: ${limit}`))
|
|
31
|
+
if (skip !== undefined) {
|
|
32
|
+
this.log(chalk.dim(` Skip: ${skip}`))
|
|
33
|
+
options.skip = skip
|
|
34
|
+
}
|
|
35
|
+
if (sort !== undefined) {
|
|
36
|
+
this.log(chalk.dim(` Sort:\n${prettyJson(sort)}`))
|
|
37
|
+
options.sort = sort
|
|
38
|
+
}
|
|
39
|
+
if (projection !== undefined) {
|
|
40
|
+
this.log(chalk.dim(` Projection:\n${prettyJson(projection)}`))
|
|
41
|
+
options.projection = projection
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}\n`))
|
|
45
|
+
|
|
46
|
+
const client = await this.db.connect()
|
|
47
|
+
const coll = client.collection(collection)
|
|
48
|
+
const results = await coll.findArray(filter, options)
|
|
49
|
+
const timestamp = new Date().toISOString()
|
|
50
|
+
const response = {
|
|
51
|
+
collection,
|
|
52
|
+
filter,
|
|
53
|
+
options,
|
|
54
|
+
results,
|
|
55
|
+
namespace: this.rtNamespace,
|
|
56
|
+
timestamp
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.debugLogger?.info?.('Find results:', results)
|
|
60
|
+
if (results?.length > 0) {
|
|
61
|
+
this.log(chalk.green(`Retrieved ${results.length} document(s) from collection '${collection}'`))
|
|
62
|
+
this.log(chalk.dim(` Searched: ${timestamp}`))
|
|
63
|
+
this.log(chalk.dim(` Results:\n${prettyJson(results)}`))
|
|
64
|
+
} else {
|
|
65
|
+
this.log(chalk.green(`No documents matching the filter criteria found in collection '${collection}'.`))
|
|
66
|
+
this.log(chalk.dim(` Searched: ${timestamp}`))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return response
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.debugLogger?.error?.('Error finding documents:', error)
|
|
72
|
+
|
|
73
|
+
const errorMessage = `Failed to find documents in collection '${collection}': ${error.message}`
|
|
74
|
+
|
|
75
|
+
this.log(chalk.red('Failed to find documents'))
|
|
76
|
+
this.log(chalk.dim(` Collection: ${collection}`))
|
|
77
|
+
this.log(chalk.dim(` Namespace: ${this.rtNamespace}`))
|
|
78
|
+
this.error(errorMessage)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
Find.description = 'Find documents in a collection based on filter criteria.'
|
|
84
|
+
|
|
85
|
+
Find.examples = [
|
|
86
|
+
'$ aio app db document find users \'{}\'',
|
|
87
|
+
'$ aio app db document find products \'{"category": "Computer Accessories"}\' --json',
|
|
88
|
+
'$ aio app db document find products \'{"name": {"$regex": "Speakers$"}}\' --sort \'{"price": -1}\' --limit 10 --skip 5 --projection \'{"name": 1, "price": 1}\'',
|
|
89
|
+
'$ aio app db doc find orders \'{"status": "pending"}\' --sort \'{"orderDate": -1}\''
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
Find.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: 'Filter criteria for the documents to find (JSON string, e.g. \'{"status": "active"}\')',
|
|
102
|
+
required: true,
|
|
103
|
+
parse: input => asObject(input, 'Filter')
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
Find.flags = {
|
|
108
|
+
...DBBaseCommand.flags,
|
|
109
|
+
limit: Flags.integer({
|
|
110
|
+
char: 'l',
|
|
111
|
+
description: 'Limit the number of documents returned, max: 100',
|
|
112
|
+
default: 20,
|
|
113
|
+
max: 100,
|
|
114
|
+
min: 0
|
|
115
|
+
}),
|
|
116
|
+
skip: Flags.integer({
|
|
117
|
+
char: 's',
|
|
118
|
+
description: 'Skip the first N documents',
|
|
119
|
+
min: 0
|
|
120
|
+
}),
|
|
121
|
+
sort: Flags.string({
|
|
122
|
+
char: 'o',
|
|
123
|
+
description: 'Sort specification as a JSON object (e.g. \'{"field": 1}\')',
|
|
124
|
+
parse: input => asObject(input, 'Sort')
|
|
125
|
+
}),
|
|
126
|
+
projection: Flags.string({
|
|
127
|
+
char: 'p',
|
|
128
|
+
description: 'Projection specification as a JSON object (e.g. \'{"field1": 1, "field2": 0}\')',
|
|
129
|
+
parse: input => asObject(input, 'Projection')
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
Find.aliases = ['app:db:doc:find']
|
|
@@ -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']
|