@genoacms/adapter-gcp 0.8.2 → 0.10.0
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/LICENSE +21 -0
- package/dist/authentication/identity-platform/descriptor.d.ts +18 -0
- package/dist/authentication/identity-platform/descriptor.js +14 -0
- package/dist/authentication/identity-platform/runtime.d.ts +4 -0
- package/dist/authentication/identity-platform/runtime.js +140 -0
- package/dist/database/descriptor.d.ts +16 -0
- package/dist/database/descriptor.js +7 -0
- package/dist/database/runtime.d.ts +5 -0
- package/dist/database/runtime.js +68 -0
- package/dist/deployment/archive.d.ts +4 -0
- package/dist/deployment/archive.js +46 -0
- package/dist/deployment/descriptor.d.ts +24 -0
- package/dist/deployment/descriptor.js +20 -0
- package/dist/deployment/functions.d.ts +16 -0
- package/dist/deployment/functions.js +66 -0
- package/dist/deployment/procedure.d.ts +4 -0
- package/dist/deployment/procedure.js +16 -0
- package/dist/deployment/settings.d.ts +22 -0
- package/dist/deployment/settings.js +58 -0
- package/dist/secrets/descriptor.d.ts +14 -0
- package/dist/secrets/descriptor.js +7 -0
- package/dist/secrets/runtime.d.ts +14 -0
- package/dist/secrets/runtime.js +147 -0
- package/dist/shared/serviceAccount.d.ts +16 -0
- package/dist/shared/serviceAccount.js +12 -0
- package/dist/storage/descriptor.d.ts +14 -0
- package/dist/storage/descriptor.js +7 -0
- package/dist/storage/runtime.d.ts +8 -0
- package/dist/storage/runtime.js +207 -0
- package/package.json +47 -28
- package/deployment/snippets/index.js +0 -8
- package/dist/config.d.ts +0 -2
- package/dist/config.js +0 -2
- package/dist/services/authorization/index.d.ts +0 -3
- package/dist/services/authorization/index.js +0 -24
- package/dist/services/database/index.d.ts +0 -7
- package/dist/services/database/index.js +0 -63
- package/dist/services/deployment/deploy.d.ts +0 -2
- package/dist/services/deployment/deploy.js +0 -116
- package/dist/services/deployment/index.d.ts +0 -4
- package/dist/services/deployment/index.js +0 -6
- package/dist/services/storage/index.d.ts +0 -12
- package/dist/services/storage/index.js +0 -87
- package/dist/services/storage/storage.d.ts +0 -3
- package/dist/services/storage/storage.js +0 -20
- package/src/config.ts +0 -4
- package/src/genoa.config.d.ts +0 -50
- package/src/services/authorization/index.ts +0 -28
- package/src/services/database/index.ts +0 -88
- package/src/services/deployment/deploy.ts +0 -126
- package/src/services/deployment/index.ts +0 -13
- package/src/services/storage/index.ts +0 -119
- package/src/services/storage/storage.ts +0 -27
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { defineRuntime } from '@genoacms/contracts';
|
|
2
|
+
import { assertValidSecretKey } from '@genoacms/contracts/secrets';
|
|
3
|
+
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
|
|
4
|
+
/**
|
|
5
|
+
* The GenoaCMS `secrets` service backed by GCP Secret Manager.
|
|
6
|
+
*
|
|
7
|
+
* Secret Manager is versioned; this contract is not. Writes add a version and reads always take
|
|
8
|
+
* `latest`, so version history exists but is never consulted. That is deliberate — building the
|
|
9
|
+
* contract on a provider-specific behavior would leave the abstraction unimplementable elsewhere.
|
|
10
|
+
* `setSecret` therefore destroys the versions it supersedes (architecture GD4). Secrets created here
|
|
11
|
+
* keep a destroyed version disabled for seven days before it is gone, so a bad overwrite can be undone
|
|
12
|
+
* by hand.
|
|
13
|
+
*/
|
|
14
|
+
export default defineRuntime({
|
|
15
|
+
create({ projectId, credentials }) {
|
|
16
|
+
const client = new SecretManagerServiceClient({
|
|
17
|
+
projectId,
|
|
18
|
+
...(credentials === undefined ? {} : { credentials })
|
|
19
|
+
});
|
|
20
|
+
const GRPC_NOT_FOUND = 5;
|
|
21
|
+
const GRPC_ALREADY_EXISTS = 6;
|
|
22
|
+
const parent = `projects/${projectId}`;
|
|
23
|
+
const secretName = (key) => `${parent}/secrets/${key}`;
|
|
24
|
+
const latestVersionName = (key) => `${secretName(key)}/versions/latest`;
|
|
25
|
+
// SEC-7, GD4
|
|
26
|
+
const VERSION_DESTROY_TTL = { seconds: 7 * 24 * 60 * 60 };
|
|
27
|
+
// SEC-7
|
|
28
|
+
const newSecret = () => ({ replication: { automatic: {} }, versionDestroyTtl: VERSION_DESTROY_TTL });
|
|
29
|
+
// SEC-8, SEC-9
|
|
30
|
+
function versionNumber(name) {
|
|
31
|
+
const number = Number(name.split('/').at(-1));
|
|
32
|
+
if (!Number.isInteger(number) || number < 1)
|
|
33
|
+
throw new Error(`secrets/unexpected-version-name: ${name}`);
|
|
34
|
+
return number;
|
|
35
|
+
}
|
|
36
|
+
// SEC-8
|
|
37
|
+
async function destroySuperseded(key, added) {
|
|
38
|
+
const addedNumber = versionNumber(added);
|
|
39
|
+
const [versions] = await client.listSecretVersions({ parent: secretName(key), filter: 'state:ENABLED' });
|
|
40
|
+
for (const version of versions) {
|
|
41
|
+
if (typeof version.name === 'string' && versionNumber(version.name) < addedNumber) {
|
|
42
|
+
await client.destroySecretVersion({ name: version.name });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// SEC-9, GD4
|
|
47
|
+
async function cleanUp(key, added) {
|
|
48
|
+
try {
|
|
49
|
+
await destroySuperseded(key, added);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
console.warn(`secrets/cleanup-failed: ${key}: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function hasStatusCode(error, code) {
|
|
56
|
+
return typeof error === 'object' && error !== null && error.code === code;
|
|
57
|
+
}
|
|
58
|
+
// SEC-5
|
|
59
|
+
async function ensureSecretExists(key) {
|
|
60
|
+
try {
|
|
61
|
+
await client.getSecret({ name: secretName(key) });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (!hasStatusCode(error, GRPC_NOT_FOUND))
|
|
65
|
+
throw error;
|
|
66
|
+
try {
|
|
67
|
+
await client.createSecret({
|
|
68
|
+
parent,
|
|
69
|
+
secretId: key,
|
|
70
|
+
secret: newSecret()
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
catch (createError) {
|
|
74
|
+
if (!hasStatusCode(createError, GRPC_ALREADY_EXISTS))
|
|
75
|
+
throw createError;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// SEC-3, SEC-4
|
|
80
|
+
const getSecret = async (key) => {
|
|
81
|
+
assertValidSecretKey(key);
|
|
82
|
+
try {
|
|
83
|
+
const [version] = await client.accessSecretVersion({ name: latestVersionName(key) });
|
|
84
|
+
const data = version.payload?.data;
|
|
85
|
+
if (data === null || data === undefined)
|
|
86
|
+
return undefined;
|
|
87
|
+
return typeof data === 'string' ? data : Buffer.from(data).toString('utf-8');
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (hasStatusCode(error, GRPC_NOT_FOUND))
|
|
91
|
+
return undefined;
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const setSecret = async (key, value) => {
|
|
96
|
+
assertValidSecretKey(key);
|
|
97
|
+
await ensureSecretExists(key);
|
|
98
|
+
const [version] = await client.addSecretVersion({
|
|
99
|
+
parent: secretName(key),
|
|
100
|
+
payload: { data: Buffer.from(value, 'utf-8') }
|
|
101
|
+
});
|
|
102
|
+
if (typeof version.name === 'string')
|
|
103
|
+
await cleanUp(key, version.name);
|
|
104
|
+
return true;
|
|
105
|
+
};
|
|
106
|
+
// SEC-6, SEC-10
|
|
107
|
+
const setSecretIfAbsent = async (key, value) => {
|
|
108
|
+
assertValidSecretKey(key);
|
|
109
|
+
try {
|
|
110
|
+
await client.createSecret({
|
|
111
|
+
parent,
|
|
112
|
+
secretId: key,
|
|
113
|
+
secret: newSecret()
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (hasStatusCode(error, GRPC_ALREADY_EXISTS))
|
|
118
|
+
return false;
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
await client.addSecretVersion({
|
|
122
|
+
parent: secretName(key),
|
|
123
|
+
payload: { data: Buffer.from(value, 'utf-8') }
|
|
124
|
+
});
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
// SEC-11
|
|
128
|
+
const deleteSecret = async (key) => {
|
|
129
|
+
assertValidSecretKey(key);
|
|
130
|
+
try {
|
|
131
|
+
await client.deleteSecret({ name: secretName(key) });
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (hasStatusCode(error, GRPC_NOT_FOUND))
|
|
136
|
+
return false;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
getSecret,
|
|
142
|
+
setSecret,
|
|
143
|
+
deleteSecret,
|
|
144
|
+
setSecretIfAbsent
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** The fields of a Google service-account key file that the client libraries read. */
|
|
2
|
+
export interface ServiceAccount {
|
|
3
|
+
type: string;
|
|
4
|
+
project_id: string;
|
|
5
|
+
private_key_id: string;
|
|
6
|
+
private_key: string;
|
|
7
|
+
client_email: string;
|
|
8
|
+
client_id: string;
|
|
9
|
+
auth_uri?: string;
|
|
10
|
+
token_uri?: string;
|
|
11
|
+
auth_provider_x509_cert_url?: string;
|
|
12
|
+
client_x509_cert_url?: string;
|
|
13
|
+
universe_domain?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function unknownOptions(options: unknown, allowed: readonly string[]): string[];
|
|
16
|
+
export declare function requireString(options: unknown, key: string): string[];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const asRecord = (options) => typeof options === 'object' && options !== null ? options : {};
|
|
2
|
+
// COM-2
|
|
3
|
+
export function unknownOptions(options, allowed) {
|
|
4
|
+
return Object.keys(asRecord(options))
|
|
5
|
+
.filter(key => !allowed.includes(key))
|
|
6
|
+
.map(key => `unknown option '${key}'`);
|
|
7
|
+
}
|
|
8
|
+
// COM-3
|
|
9
|
+
export function requireString(options, key) {
|
|
10
|
+
const value = asRecord(options)[key];
|
|
11
|
+
return typeof value === 'string' && value !== '' ? [] : [`${key} is required and must be a non-empty string`];
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Secret } from '@genoacms/contracts';
|
|
2
|
+
import { type ServiceAccount } from '../shared/serviceAccount.js';
|
|
3
|
+
export interface GcpStorageOptions {
|
|
4
|
+
projectId: string;
|
|
5
|
+
/** Omitted: Application Default Credentials, i.e. the runtime's own identity. */
|
|
6
|
+
credentials?: Secret<ServiceAccount>;
|
|
7
|
+
}
|
|
8
|
+
declare module '@genoacms/contracts' {
|
|
9
|
+
interface StorageAdapters {
|
|
10
|
+
'@genoacms/adapter-gcp/storage': GcpStorageOptions;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
declare const _default: import("@genoacms/contracts").AdapterDescriptor<"storage", GcpStorageOptions>;
|
|
14
|
+
export default _default;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { defineStorageAdapter } from '@genoacms/contracts';
|
|
2
|
+
import { unknownOptions, requireString } from '../shared/serviceAccount.js';
|
|
3
|
+
export default defineStorageAdapter({
|
|
4
|
+
runtime: '@genoacms/adapter-gcp/storage/runtime',
|
|
5
|
+
secretOptions: { credentials: 'json' },
|
|
6
|
+
validate: options => [...unknownOptions(options, ['projectId', 'credentials']), ...requireString(options, 'projectId')]
|
|
7
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Adapter } from '@genoacms/contracts/storage';
|
|
2
|
+
import type { GcpStorageOptions } from './descriptor.js';
|
|
3
|
+
/**
|
|
4
|
+
* One GCS provider. Each construction owns its client and its credential, so two providers on this
|
|
5
|
+
* adapter — two GCP projects — never share either.
|
|
6
|
+
*/
|
|
7
|
+
declare const _default: import("@genoacms/contracts").AdapterRuntime<GcpStorageOptions, Adapter>;
|
|
8
|
+
export default _default;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { defineRuntime } from '@genoacms/contracts';
|
|
2
|
+
import { PreconditionFailedError, ObjectNotFoundError, NotADirectoryError, isDirectoryName } from '@genoacms/contracts/storage';
|
|
3
|
+
import { Storage } from '@google-cloud/storage';
|
|
4
|
+
const HTTP_PRECONDITION_FAILED = 412;
|
|
5
|
+
const HTTP_NOT_FOUND = 404;
|
|
6
|
+
const DELETE_CONCURRENCY = 10;
|
|
7
|
+
const hasStatus = (error, status) => error?.code === status;
|
|
8
|
+
// STO-4, STO-7, STOR-16
|
|
9
|
+
async function rejectingMissing(reference, call) {
|
|
10
|
+
try {
|
|
11
|
+
return await call();
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (hasStatus(error, HTTP_NOT_FOUND))
|
|
15
|
+
throw new ObjectNotFoundError(reference);
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// STO-9
|
|
20
|
+
function listingQuery(name, { limit, startAfter, pageToken }) {
|
|
21
|
+
return {
|
|
22
|
+
prefix: name,
|
|
23
|
+
delimiter: '/',
|
|
24
|
+
autoPaginate: false,
|
|
25
|
+
...(limit === undefined ? {} : { maxResults: limit }),
|
|
26
|
+
...(startAfter === undefined ? {} : { startOffset: startAfter }),
|
|
27
|
+
...(pageToken === undefined ? {} : { pageToken })
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// STO-11, GF38
|
|
31
|
+
function everyDepthPage(prefix, pageToken) {
|
|
32
|
+
return pageToken === undefined ? { prefix, autoPaginate: false } : { prefix, autoPaginate: false, pageToken };
|
|
33
|
+
}
|
|
34
|
+
// STO-11, GF29
|
|
35
|
+
async function deleteAtMost(concurrency, files) {
|
|
36
|
+
let next = 0;
|
|
37
|
+
let failure;
|
|
38
|
+
const work = async () => {
|
|
39
|
+
while (failure === undefined && next < files.length) {
|
|
40
|
+
const file = files[next++];
|
|
41
|
+
try {
|
|
42
|
+
await file.delete();
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
failure ??= { error };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, files.length) }, work));
|
|
50
|
+
if (failure !== undefined)
|
|
51
|
+
throw failure.error;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* One GCS provider. Each construction owns its client and its credential, so two providers on this
|
|
55
|
+
* adapter — two GCP projects — never share either.
|
|
56
|
+
*/
|
|
57
|
+
export default defineRuntime({
|
|
58
|
+
create({ projectId, credentials }, ctx) {
|
|
59
|
+
const storage = new Storage(credentials === undefined ? { projectId } : { projectId, credentials });
|
|
60
|
+
const registered = new Set(ctx.resources);
|
|
61
|
+
const getBucket = (name) => {
|
|
62
|
+
if (!registered.has(name))
|
|
63
|
+
throw new Error('bucket-unregistered');
|
|
64
|
+
return storage.bucket(name);
|
|
65
|
+
};
|
|
66
|
+
// STO-3, STOR-3
|
|
67
|
+
const requireDirectoryName = (reference, { allowRoot }) => {
|
|
68
|
+
if (!isDirectoryName(reference.name) || (!allowRoot && reference.name === ''))
|
|
69
|
+
throw new NotADirectoryError(reference);
|
|
70
|
+
};
|
|
71
|
+
const getDirectoryBucket = (reference, options) => {
|
|
72
|
+
const bucketInstance = getBucket(reference.bucket);
|
|
73
|
+
requireDirectoryName(reference, options);
|
|
74
|
+
return bucketInstance;
|
|
75
|
+
};
|
|
76
|
+
// STO-4
|
|
77
|
+
const getObject = async ({ bucket, name }) => {
|
|
78
|
+
const bucketInstance = getBucket(bucket);
|
|
79
|
+
const file = bucketInstance.file(name);
|
|
80
|
+
let version;
|
|
81
|
+
try {
|
|
82
|
+
const [metadata] = await file.getMetadata();
|
|
83
|
+
version = metadata.generation === undefined ? undefined : String(metadata.generation);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (hasStatus(error, HTTP_NOT_FOUND))
|
|
87
|
+
throw new ObjectNotFoundError({ bucket, name });
|
|
88
|
+
version = undefined;
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
data: file.createReadStream(),
|
|
92
|
+
version
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
const getPublicURL = async ({ bucket, name }) => {
|
|
96
|
+
const bucketInstance = getBucket(bucket);
|
|
97
|
+
const file = bucketInstance.file(name);
|
|
98
|
+
return file.publicUrl();
|
|
99
|
+
};
|
|
100
|
+
const getSignedURL = async ({ bucket, name }, expires) => {
|
|
101
|
+
const bucketInstance = getBucket(bucket);
|
|
102
|
+
const file = bucketInstance.file(name);
|
|
103
|
+
const [url] = await file.getSignedUrl({
|
|
104
|
+
action: 'read',
|
|
105
|
+
expires
|
|
106
|
+
});
|
|
107
|
+
return url;
|
|
108
|
+
};
|
|
109
|
+
const uploadObject = async ({ bucket, name }, stream, options) => {
|
|
110
|
+
const bucketInstance = getBucket(bucket);
|
|
111
|
+
const file = bucketInstance.file(name);
|
|
112
|
+
const { ifVersion, ifAbsent } = options ?? {};
|
|
113
|
+
// STO-6
|
|
114
|
+
const generation = ifAbsent === true ? 0 : ifVersion === undefined ? undefined : Number(ifVersion);
|
|
115
|
+
try {
|
|
116
|
+
await file.save(stream, generation === undefined
|
|
117
|
+
? {}
|
|
118
|
+
: { preconditionOpts: { ifGenerationMatch: generation } });
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (hasStatus(error, HTTP_PRECONDITION_FAILED)) {
|
|
122
|
+
throw new PreconditionFailedError({ bucket, name }, ifAbsent === true
|
|
123
|
+
? 'object already exists'
|
|
124
|
+
: 'object changed since it was read');
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const moveObject = async ({ bucket, name }, newName) => {
|
|
130
|
+
const bucketInstance = getBucket(bucket);
|
|
131
|
+
const file = bucketInstance.file(name);
|
|
132
|
+
await file.move(newName);
|
|
133
|
+
};
|
|
134
|
+
const deleteObject = async ({ bucket, name }) => {
|
|
135
|
+
const bucketInstance = getBucket(bucket);
|
|
136
|
+
const file = bucketInstance.file(name);
|
|
137
|
+
await rejectingMissing({ bucket, name }, async () => await file.delete());
|
|
138
|
+
};
|
|
139
|
+
// STO-9, GF21
|
|
140
|
+
const listDirectory = async (reference, listingParams = {}) => {
|
|
141
|
+
const bucketInstance = getDirectoryBucket(reference, { allowRoot: true });
|
|
142
|
+
const { bucket, name } = reference;
|
|
143
|
+
if (listingParams.limit === 0)
|
|
144
|
+
return { files: [], directories: [] };
|
|
145
|
+
const [listed, nextQuery, apiResponse] = (await bucketInstance.getFiles(listingQuery(name, listingParams)));
|
|
146
|
+
const skipped = (itemName) => itemName === name || itemName === listingParams.startAfter;
|
|
147
|
+
const files = listed.filter(file => !file.name.endsWith('.folderPlaceholder') && !skipped(file.name));
|
|
148
|
+
const prefixes = (apiResponse?.prefixes ?? []).filter(item => !skipped(item));
|
|
149
|
+
const next = nextQuery?.pageToken;
|
|
150
|
+
return {
|
|
151
|
+
...(next === undefined ? {} : { next }),
|
|
152
|
+
files: files.map((file) => {
|
|
153
|
+
return {
|
|
154
|
+
name: file.name,
|
|
155
|
+
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
|
|
156
|
+
size: file.metadata.size ? parseInt(file.metadata.size) : 0,
|
|
157
|
+
lastModified: new Date(file.metadata.updated)
|
|
158
|
+
};
|
|
159
|
+
}),
|
|
160
|
+
directories: prefixes.map(i => {
|
|
161
|
+
const object = {
|
|
162
|
+
bucket,
|
|
163
|
+
name: i
|
|
164
|
+
};
|
|
165
|
+
return object;
|
|
166
|
+
})
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
// STO-10, CD10
|
|
170
|
+
const createDirectory = async (reference) => {
|
|
171
|
+
const bucketInstance = getDirectoryBucket(reference, { allowRoot: false });
|
|
172
|
+
const file = bucketInstance.file(`${reference.name}.folderPlaceholder`);
|
|
173
|
+
await file.save('');
|
|
174
|
+
};
|
|
175
|
+
// STO-11, GD7, GF29
|
|
176
|
+
const deleteDirectory = async (reference) => {
|
|
177
|
+
const bucketInstance = getDirectoryBucket(reference, { allowRoot: false });
|
|
178
|
+
let pageToken;
|
|
179
|
+
do {
|
|
180
|
+
const [files, nextQuery] = await bucketInstance.getFiles(everyDepthPage(reference.name, pageToken));
|
|
181
|
+
await deleteAtMost(DELETE_CONCURRENCY, files);
|
|
182
|
+
pageToken = nextQuery?.pageToken;
|
|
183
|
+
} while (pageToken !== undefined);
|
|
184
|
+
};
|
|
185
|
+
// STO-12, GD7, GF22
|
|
186
|
+
const moveDirectory = async (reference, newName) => {
|
|
187
|
+
const bucketInstance = getDirectoryBucket(reference, { allowRoot: false });
|
|
188
|
+
requireDirectoryName({ bucket: reference.bucket, name: newName }, { allowRoot: false });
|
|
189
|
+
const { name } = reference;
|
|
190
|
+
const [files] = await bucketInstance.getFiles({ prefix: name });
|
|
191
|
+
for (const file of files)
|
|
192
|
+
await file.move(newName + file.name.slice(name.length));
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
getObject,
|
|
196
|
+
getPublicURL,
|
|
197
|
+
getSignedURL,
|
|
198
|
+
uploadObject,
|
|
199
|
+
moveObject,
|
|
200
|
+
deleteObject,
|
|
201
|
+
listDirectory,
|
|
202
|
+
createDirectory,
|
|
203
|
+
deleteDirectory,
|
|
204
|
+
moveDirectory
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
});
|
package/package.json
CHANGED
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genoacms/adapter-gcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Implementation of abstraction layer of GenoaCMS for GCP",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/GenoaCMS/
|
|
8
|
+
"url": "git+https://github.com/GenoaCMS/genoacms.git",
|
|
9
|
+
"directory": "packages/adapter-gcp"
|
|
9
10
|
},
|
|
10
11
|
"author": {
|
|
11
12
|
"name": "Filip Holčík",
|
|
12
13
|
"email": "filip.holcik.official@gmail.com"
|
|
13
14
|
},
|
|
14
|
-
"license": "
|
|
15
|
+
"license": "MIT",
|
|
15
16
|
"bugs": {
|
|
16
|
-
"url": "https://github.com/GenoaCMS/
|
|
17
|
+
"url": "https://github.com/GenoaCMS/genoacms/issues"
|
|
17
18
|
},
|
|
18
|
-
"homepage": "https://github.com/GenoaCMS/adapter-gcp#readme",
|
|
19
|
+
"homepage": "https://github.com/GenoaCMS/genoacms/tree/main/packages/adapter-gcp#readme",
|
|
19
20
|
"publishConfig": {
|
|
20
21
|
"provenance": true
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|
|
23
|
-
"@genoacms/cloudabstraction": "^0.8.2",
|
|
24
24
|
"@google-cloud/firestore": "^7.1.0",
|
|
25
25
|
"@google-cloud/functions": "^3.4.0",
|
|
26
|
-
"@google-cloud/
|
|
26
|
+
"@google-cloud/secret-manager": "^7.0.0",
|
|
27
27
|
"@google-cloud/storage": "^7.7.0",
|
|
28
|
-
"archiver": "^7.0.0"
|
|
28
|
+
"archiver": "^7.0.0",
|
|
29
|
+
"google-auth-library": "^9.15.1",
|
|
30
|
+
"@genoacms/contracts": "^0.0.3",
|
|
31
|
+
"@genoacms/sveltekit-adapter-cloud-run-functions": "^1.0.1"
|
|
29
32
|
},
|
|
30
33
|
"devDependencies": {
|
|
31
34
|
"@types/archiver": "^6.0.2",
|
|
@@ -37,38 +40,54 @@
|
|
|
37
40
|
"eslint-plugin-promise": "^6.1.1",
|
|
38
41
|
"rimraf": "^5.0.5",
|
|
39
42
|
"typescript": "^5.2.2",
|
|
40
|
-
"vitest": "^
|
|
41
|
-
|
|
42
|
-
"peerDependencies": {
|
|
43
|
-
"@genoacms/sveltekit-adapter-cloud-run-functions": "^1.0.0"
|
|
43
|
+
"vitest": "^3.2.7",
|
|
44
|
+
"@genoacms/conformance": "^0.0.3"
|
|
44
45
|
},
|
|
45
46
|
"files": [
|
|
46
|
-
"src",
|
|
47
|
-
"deployment",
|
|
48
47
|
"dist"
|
|
49
48
|
],
|
|
50
49
|
"exports": {
|
|
51
|
-
"./
|
|
52
|
-
"
|
|
53
|
-
"
|
|
50
|
+
"./storage": {
|
|
51
|
+
"types": "./dist/storage/descriptor.d.ts",
|
|
52
|
+
"import": "./dist/storage/descriptor.js"
|
|
53
|
+
},
|
|
54
|
+
"./storage/runtime": {
|
|
55
|
+
"types": "./dist/storage/runtime.d.ts",
|
|
56
|
+
"import": "./dist/storage/runtime.js"
|
|
54
57
|
},
|
|
55
58
|
"./database": {
|
|
56
|
-
"
|
|
57
|
-
"
|
|
59
|
+
"types": "./dist/database/descriptor.d.ts",
|
|
60
|
+
"import": "./dist/database/descriptor.js"
|
|
58
61
|
},
|
|
59
|
-
"./
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
+
"./database/runtime": {
|
|
63
|
+
"types": "./dist/database/runtime.d.ts",
|
|
64
|
+
"import": "./dist/database/runtime.js"
|
|
62
65
|
},
|
|
63
|
-
"./
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
+
"./secrets": {
|
|
67
|
+
"types": "./dist/secrets/descriptor.d.ts",
|
|
68
|
+
"import": "./dist/secrets/descriptor.js"
|
|
69
|
+
},
|
|
70
|
+
"./secrets/runtime": {
|
|
71
|
+
"types": "./dist/secrets/runtime.d.ts",
|
|
72
|
+
"import": "./dist/secrets/runtime.js"
|
|
73
|
+
},
|
|
74
|
+
"./authentication/identity-platform": {
|
|
75
|
+
"types": "./dist/authentication/identity-platform/descriptor.d.ts",
|
|
76
|
+
"import": "./dist/authentication/identity-platform/descriptor.js"
|
|
77
|
+
},
|
|
78
|
+
"./authentication/identity-platform/runtime": {
|
|
79
|
+
"types": "./dist/authentication/identity-platform/runtime.d.ts",
|
|
80
|
+
"import": "./dist/authentication/identity-platform/runtime.js"
|
|
81
|
+
},
|
|
82
|
+
"./deployment": {
|
|
83
|
+
"types": "./dist/deployment/descriptor.d.ts",
|
|
84
|
+
"import": "./dist/deployment/descriptor.js"
|
|
66
85
|
}
|
|
67
86
|
},
|
|
68
87
|
"scripts": {
|
|
69
88
|
"build": "rimraf dist && tsc",
|
|
70
|
-
"test
|
|
71
|
-
"test:
|
|
72
|
-
"test:
|
|
89
|
+
"test": "vitest run",
|
|
90
|
+
"test:conformance": "vitest run test/conformance.test.ts",
|
|
91
|
+
"test:contract": "vitest run test/contract test/conformance.test.ts"
|
|
73
92
|
}
|
|
74
93
|
}
|
package/dist/config.d.ts
DELETED
package/dist/config.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { getProvider } from '@genoacms/cloudabstraction';
|
|
2
|
-
import { ProjectsClient } from '@google-cloud/resource-manager';
|
|
3
|
-
const ADAPTER_PATH = '@genoacms/adapter-gcp/authorization';
|
|
4
|
-
const provider = getProvider('authorization', ADAPTER_PATH);
|
|
5
|
-
const projectId = provider.projectId;
|
|
6
|
-
const resourceManager = new ProjectsClient({
|
|
7
|
-
projectId,
|
|
8
|
-
credentials: provider.credentials
|
|
9
|
-
});
|
|
10
|
-
const isEmailAdmins = async (email) => {
|
|
11
|
-
const resource = `projects/${projectId}`;
|
|
12
|
-
const role = resource + '/roles/genoacms';
|
|
13
|
-
const data = await resourceManager.getIamPolicy({ resource });
|
|
14
|
-
const policy = data[0];
|
|
15
|
-
if ((policy.bindings) == null)
|
|
16
|
-
throw new Error('no-bindings');
|
|
17
|
-
const adminRole = policy.bindings.find(binding => binding.role === role);
|
|
18
|
-
if (adminRole == null)
|
|
19
|
-
throw new Error('no-admin-role');
|
|
20
|
-
if (adminRole.members == null)
|
|
21
|
-
throw new Error('no-principals');
|
|
22
|
-
return adminRole.members.includes(`user:${email}`);
|
|
23
|
-
};
|
|
24
|
-
export { isEmailAdmins };
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { Adapter } from '@genoacms/cloudabstraction/database';
|
|
2
|
-
declare const createDocument: Adapter['createDocument'];
|
|
3
|
-
declare const getCollection: Adapter['getCollection'];
|
|
4
|
-
declare const getDocument: Adapter['getDocument'];
|
|
5
|
-
declare const updateDocument: Adapter['updateDocument'];
|
|
6
|
-
declare const deleteDocument: Adapter['deleteDocument'];
|
|
7
|
-
export { createDocument, getDocument, getCollection, updateDocument, deleteDocument };
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { Firestore } from '@google-cloud/firestore';
|
|
2
|
-
import { getProvider } from '@genoacms/cloudabstraction';
|
|
3
|
-
const ADAPTER_PATH = '@genoacms/adapter-gcp/database';
|
|
4
|
-
const provider = getProvider('database', ADAPTER_PATH);
|
|
5
|
-
const firestore = new Firestore({
|
|
6
|
-
credentials: provider.credentials,
|
|
7
|
-
databaseId: provider.databaseId,
|
|
8
|
-
projectId: provider.projectId
|
|
9
|
-
});
|
|
10
|
-
const createDocument = async (reference, data) => {
|
|
11
|
-
const document = await firestore.collection(reference.name).add(data);
|
|
12
|
-
const documentReference = {
|
|
13
|
-
collection: reference,
|
|
14
|
-
id: document.id
|
|
15
|
-
};
|
|
16
|
-
return {
|
|
17
|
-
reference: documentReference,
|
|
18
|
-
data
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
const getCollection = async (reference) => {
|
|
22
|
-
const collection = await firestore.collection(reference.name).get();
|
|
23
|
-
const documents = [];
|
|
24
|
-
collection.forEach(document => {
|
|
25
|
-
const documentData = {};
|
|
26
|
-
Object.keys(reference.schema.properties).forEach(key => {
|
|
27
|
-
documentData[key] = document.get(key);
|
|
28
|
-
});
|
|
29
|
-
documents.push({
|
|
30
|
-
reference: {
|
|
31
|
-
collection: reference,
|
|
32
|
-
id: document.id
|
|
33
|
-
},
|
|
34
|
-
data: document.data()
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
return documents;
|
|
38
|
-
};
|
|
39
|
-
const getDocument = async ({ collection, id }) => {
|
|
40
|
-
const document = await firestore.collection(collection.name).doc(id).get();
|
|
41
|
-
if (!document.exists)
|
|
42
|
-
return undefined;
|
|
43
|
-
const documentReference = {
|
|
44
|
-
collection,
|
|
45
|
-
id
|
|
46
|
-
};
|
|
47
|
-
const documentSnapshot = {
|
|
48
|
-
reference: documentReference,
|
|
49
|
-
data: document.data()
|
|
50
|
-
};
|
|
51
|
-
return documentSnapshot;
|
|
52
|
-
};
|
|
53
|
-
const updateDocument = async (reference, document) => {
|
|
54
|
-
await firestore.collection(reference.collection.name).doc(reference.id).update(document);
|
|
55
|
-
return {
|
|
56
|
-
reference,
|
|
57
|
-
data: document
|
|
58
|
-
};
|
|
59
|
-
};
|
|
60
|
-
const deleteDocument = async (reference) => {
|
|
61
|
-
await firestore.collection(reference.collection.name).doc(reference.id).delete();
|
|
62
|
-
};
|
|
63
|
-
export { createDocument, getDocument, getCollection, updateDocument, deleteDocument };
|