Skip to content

Commit 6d11272

Browse files
thiyaguk09iennae
andauthored
feat(storage): add Object Contexts samples and system tests (GoogleCloudPlatform#4276)
* Feat(storage): add Object Contexts samples and system tests - Add `setObjectContexts.js` to demonstrate CRUD and deletion of contexts. - Add `getObjectContexts.js` to show retrieval of structured metadata. - Add `listObjectsWithContextFilter.js` to demonstrate server-side filtering. - Implement comprehensive system tests in `files.test.js` covering presence, absence (-), and existence (:) filter operators. - Ensure samples use correct 'contexts' field with 'custom' map structure. * code refactor * fix: typo corrections * test(storage): refactor to state-based assertions --------- Co-authored-by: Jennifer Davis <sigje@google.com>
1 parent fd1aed3 commit 6d11272

4 files changed

Lines changed: 394 additions & 0 deletions

File tree

storage/getObjectContexts.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
// sample-metadata:
18+
// title: Get Object Contexts
19+
// description: Retrieves the structured Object Contexts from an object.
20+
// usage: node getObjectContexts.js <BUCKET_NAME> <FILE_NAME>
21+
22+
/**
23+
* This application demonstrates how to retrieve the 'contexts' field from a file
24+
* in Google Cloud Storage.
25+
*/
26+
27+
function main(bucketName = 'my-bucket', fileName = 'test.txt') {
28+
// [START storage_get_object_contexts]
29+
/**
30+
* TODO(developer): Uncomment the following lines before running the sample.
31+
*/
32+
// The ID of your GCS bucket
33+
// const bucketName = 'your-unique-bucket-name';
34+
35+
// The ID of your GCS file
36+
// const fileName = 'your-file-name';
37+
38+
// Imports the Google Cloud client library
39+
const {Storage} = require('@google-cloud/storage');
40+
41+
// Creates a client
42+
const storage = new Storage();
43+
44+
async function getObjectContexts() {
45+
// Gets the metadata for the file
46+
const [metadata] = await storage
47+
.bucket(bucketName)
48+
.file(fileName)
49+
.getMetadata();
50+
51+
// Contexts are stored in metadata.contexts.custom
52+
if (metadata.contexts && metadata.contexts.custom) {
53+
console.log(`Object Contexts for ${fileName}:`);
54+
55+
// Iterate through the custom contexts to show values and timestamps
56+
for (const [key, details] of Object.entries(metadata.contexts.custom)) {
57+
console.log(`- Key: ${key}`);
58+
console.log(` Value: ${details.value}`);
59+
console.log(` Created: ${details.createTime}`);
60+
console.log(` Updated: ${details.updateTime}`);
61+
}
62+
} else {
63+
console.log(`No Object Contexts found for ${fileName}.`);
64+
}
65+
}
66+
67+
getObjectContexts().catch(console.error);
68+
// [END storage_get_object_contexts]
69+
}
70+
71+
main(...process.argv.slice(2));

storage/listObjectContexts.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
// sample-metadata:
18+
// title: List Objects with Context Filter
19+
// description: Lists objects in a bucket that match specific custom contexts.
20+
// usage: node listObjectContexts.js <BUCKET_NAME>
21+
22+
/**
23+
* This application demonstrates how to list objects in a bucket while filtering
24+
* by their custom 'contexts' metadata.
25+
*/
26+
27+
function main(bucketName = 'my-bucket') {
28+
// [START storage_list_object_contexts]
29+
/**
30+
* TODO(developer): Uncomment the following lines before running the sample.
31+
*/
32+
// The ID of your GCS bucket
33+
// const bucketName = 'your-unique-bucket-name';
34+
35+
// Imports the Google Cloud client library
36+
const {Storage} = require('@google-cloud/storage');
37+
38+
// Creates a client
39+
const storage = new Storage();
40+
41+
async function listObjectContexts() {
42+
// Define the filter for contexts.
43+
const bucket = storage.bucket(bucketName);
44+
45+
/**
46+
* List any object that has a context with the specified key and value.
47+
* Syntax: contexts."<key>"="<value>"
48+
*/
49+
const filterByValue = 'contexts."priority"="high"';
50+
const [filesByValue] = await bucket.getFiles({
51+
filter: filterByValue,
52+
});
53+
54+
console.log(`\nFiles matching filter [${filterByValue}]:`);
55+
filesByValue.forEach(file => console.log(` - ${file.name}`));
56+
57+
/**
58+
* List any object that has a context with the specified key attached.
59+
* Syntax: contexts."<key>":*
60+
*/
61+
const filterByExistence = 'contexts."team-owner":*';
62+
const [filesWithKey] = await bucket.getFiles({
63+
filter: filterByExistence,
64+
});
65+
66+
console.log(
67+
`\nFiles with the "team-owner" context key [${filterByExistence}]:`
68+
);
69+
filesWithKey.forEach(file => console.log(` - ${file.name}`));
70+
71+
/**
72+
* List any object that does not have a context with the specified key and value attached.
73+
* Syntax: -contexts."<key>"="<value>"
74+
*/
75+
const absenceOfValuePair = '-contexts."priority"="high"';
76+
const [filesNoHighPriority] = await bucket.getFiles({
77+
filter: absenceOfValuePair,
78+
});
79+
80+
console.log(
81+
`\nFiles matching absence of value pair [${absenceOfValuePair}]:`
82+
);
83+
filesNoHighPriority.forEach(file => console.log(` - ${file.name}`));
84+
85+
/**
86+
* List any object that does not have a context with the specified key attached.
87+
* Syntax: -contexts."<key>":*
88+
*/
89+
const absenceOfKey = '-contexts."team-owner":*';
90+
const [filesNoTeamOwner] = await bucket.getFiles({
91+
filter: absenceOfKey,
92+
});
93+
94+
console.log(
95+
`\nFiles matching absence of key regardless of value [${absenceOfKey}]:`
96+
);
97+
filesNoTeamOwner.forEach(file => console.log(` - ${file.name}`));
98+
}
99+
100+
listObjectContexts().catch(console.error);
101+
// [END storage_list_object_contexts]
102+
}
103+
104+
main(...process.argv.slice(2));

storage/setObjectContexts.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
// sample-metadata:
18+
// title: Set Object Contexts
19+
// description: Sets custom metadata (contexts) on an object.
20+
// usage: node setObjectContexts.js <BUCKET_NAME> <FILE_NAME>
21+
22+
/**
23+
* This application demonstrates how to set, update, and delete object contexts (metadata) on a file
24+
* in Google Cloud Storage.
25+
*/
26+
27+
function main(bucketName = 'my-bucket', fileName = 'test.txt') {
28+
// [START storage_set_object_contexts]
29+
/**
30+
* TODO(developer): Uncomment the following lines before running the sample.
31+
*/
32+
// The ID of your GCS bucket
33+
// const bucketName = 'your-unique-bucket-name';
34+
35+
// The ID of your GCS file
36+
// const fileName = 'your-file-name';
37+
38+
// Imports the Google Cloud client library
39+
const {Storage} = require('@google-cloud/storage');
40+
41+
// Creates a client
42+
const storage = new Storage();
43+
44+
async function setObjectContexts() {
45+
const file = storage.bucket(bucketName).file(fileName);
46+
47+
// Create/Update Object Contexts
48+
// Object Contexts live in the 'contexts' field, not the 'metadata' field.
49+
const [metadata] = await file.setMetadata({
50+
contexts: {
51+
custom: {
52+
'team-owner': {value: 'storage-team'},
53+
priority: {value: 'high'},
54+
},
55+
},
56+
});
57+
58+
console.log(`Updated Object Contexts for ${fileName}:`);
59+
console.log(JSON.stringify(metadata.contexts, null, 2));
60+
}
61+
62+
setObjectContexts().catch(console.error);
63+
// [END storage_set_object_contexts]
64+
}
65+
66+
main(...process.argv.slice(2));

storage/system-test/files.test.js

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
'use strict';
16+
17+
const fs = require('fs');
18+
const path = require('path');
19+
const {Storage} = require('@google-cloud/storage');
20+
const {assert} = require('chai');
21+
const {before, after, it, describe} = require('mocha');
22+
const cp = require('child_process');
23+
const uuid = require('uuid');
24+
const {promisify} = require('util');
25+
26+
const execSync = cmd => cp.execSync(cmd, {encoding: 'utf-8'});
27+
28+
const storage = new Storage();
29+
const cwd = path.join(__dirname, '..');
30+
const bucketName = generateName();
31+
const bucket = storage.bucket(bucketName);
32+
const fileName = 'test.txt';
33+
const filePath = path.join(cwd, 'resources', fileName);
34+
const downloadFilePath = path.join(cwd, 'downloaded.txt');
35+
36+
describe('file', () => {
37+
before(async () => {
38+
await bucket.create();
39+
});
40+
41+
after(async () => {
42+
await promisify(fs.unlink)(downloadFilePath).catch(console.error);
43+
// Try deleting all files twice, just to make sure
44+
await bucket.deleteFiles({force: true}).catch(console.error);
45+
await bucket.deleteFiles({force: true}).catch(console.error);
46+
await bucket.delete().catch(console.error);
47+
});
48+
49+
describe('Object Contexts', () => {
50+
const contextFile = bucket.file(fileName);
51+
52+
beforeEach(async () => {
53+
await bucket.upload(filePath, {destination: fileName});
54+
55+
await contextFile.setMetadata({
56+
contexts: {
57+
custom: {
58+
'team-owner': {value: 'storage-team'},
59+
priority: {value: 'high'},
60+
},
61+
},
62+
});
63+
});
64+
65+
it('should set object contexts', async () => {
66+
const output = execSync(
67+
`node setObjectContexts.js ${bucketName} ${fileName}`
68+
);
69+
// Verify Initial Creation
70+
assert.include(output, `Updated Object Contexts for ${fileName}:`);
71+
assert.include(output, '"team-owner":');
72+
assert.include(output, '"value": "storage-team"');
73+
assert.include(output, '"priority":');
74+
assert.include(output, '"value": "high"');
75+
assert.include(output, '"createTime":');
76+
assert.include(output, '"updateTime":');
77+
78+
const [metadata] = await contextFile.getMetadata();
79+
const customContexts = metadata.contexts?.custom || {};
80+
81+
assert.strictEqual(customContexts['priority']?.value, 'high');
82+
assert.strictEqual(customContexts['team-owner']?.value, 'storage-team');
83+
});
84+
85+
it('should get object contexts', async () => {
86+
const output = execSync(
87+
`node getObjectContexts.js ${bucketName} ${fileName}`
88+
);
89+
assert.include(output, `Object Contexts for ${fileName}:`);
90+
assert.include(output, 'Key: priority');
91+
assert.include(output, 'Value: high');
92+
assert.include(output, 'Key: team-owner');
93+
assert.include(output, 'Value: storage-team');
94+
95+
const [metadata] = await contextFile.getMetadata();
96+
const customContexts = metadata.contexts?.custom || {};
97+
98+
assert.strictEqual(customContexts['priority'].value, 'high');
99+
assert.strictEqual(customContexts['team-owner'].value, 'storage-team');
100+
});
101+
102+
it('should list objects with context filters', async () => {
103+
const noContextFileName = `no-context-${fileName}`;
104+
await bucket.upload(filePath, {destination: noContextFileName});
105+
// Ensure it has no contexts
106+
await bucket
107+
.file(noContextFileName)
108+
.setMetadata({contexts: {custom: null}});
109+
110+
const output = execSync(`node listObjectContexts.js ${bucketName}`);
111+
112+
// Testing Existence of Value Pair (contexts."key"="val")
113+
assert.include(
114+
output,
115+
'Files matching filter [contexts."priority"="high"]'
116+
);
117+
assert.include(output, ` - ${fileName}`);
118+
119+
// Testing Existence of Key (contexts."key":*)
120+
assert.include(output, 'Files with the "team-owner" context key');
121+
assert.include(output, ` - ${fileName}`);
122+
123+
// Testing Absence of Value Pair (-contexts."key"="val")
124+
assert.include(
125+
output,
126+
'Files matching absence of value pair [-contexts."priority"="high"]'
127+
);
128+
assert.include(output, ` - ${noContextFileName}`);
129+
130+
// Testing Absence of Key (-contexts."key":*)
131+
assert.include(
132+
output,
133+
'Files matching absence of key regardless of value [-contexts."team-owner":*]'
134+
);
135+
assert.include(output, ` - ${noContextFileName}`);
136+
137+
const [files] = await bucket.getFiles();
138+
const targetFile = files.find(f => f.name === fileName);
139+
140+
assert.exists(targetFile);
141+
const [metadata] = await targetFile.getMetadata();
142+
143+
// Verify the state that the list filter is supposed to find
144+
assert.strictEqual(metadata.contexts?.custom?.priority?.value, 'high');
145+
146+
await bucket.file(noContextFileName).delete();
147+
});
148+
});
149+
});
150+
151+
function generateName() {
152+
return `nodejs-storage-samples-${uuid.v4()}`;
153+
}

0 commit comments

Comments
 (0)