Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(datasource-google-drive): add google drive datasource #3

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
674 changes: 674 additions & 0 deletions packages/datasource-google-drive/LICENSE

Large diffs are not rendered by default.

Empty file.
8 changes: 8 additions & 0 deletions packages/datasource-google-drive/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* eslint-disable import/no-relative-packages */
import jestConfig from '../../jest.config';

export default {
...jestConfig,
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
testMatch: ['<rootDir>/test/**/*.test.ts'],
};
29 changes: 29 additions & 0 deletions packages/datasource-google-drive/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@forestadmin/datasource-google-drive-experimental",
"version": "1.0.0",
"main": "dist/index.js",
"license": "GPL-3.0",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ForestAdmin/agent-nodejs.git",
"directory": "packages/datasource-google-drive"
},
"dependencies": {
"@forestadmin/datasource-toolkit": "1.5.0",
"@googleapis/drive": "^5.1.0"
},
"files": [
"dist/**/*.js",
"dist/**/*.d.ts"
],
"scripts": {
"build": "tsc",
"build:watch": "tsc --watch",
"clean": "rm -rf coverage dist",
"lint": "eslint src test",
"test": "jest"
}
}
144 changes: 144 additions & 0 deletions packages/datasource-google-drive/src/collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import {
AggregateResult,
Aggregation,
BaseCollection,
Caller,
DataSource,
FieldSchema,
Filter,
Operator,
PaginatedFilter,
Projection,
RecordData,
} from '@forestadmin/datasource-toolkit';
import { drive, drive_v3 } from '@googleapis/drive';

import { Options } from './datasource';
import queryStringFromConditionTree from './utils/query-converter';

export default class GoogleDriveCollection extends BaseCollection {
private static supportedOperators = new Set<Operator>([
'Blank',
'Contains',
'LessThan',
'Equal',
'GreaterThan',
'In',
'IncludesAll',
// 'ShorterThan',
// 'LongerThan',
'Present',
'NotContains',
'NotEqual',
'NotIn',
]);

private static schema: Record<string, FieldSchema> = {
id: {
type: 'Column',
columnType: 'String',
isPrimaryKey: true,
},
name: {
type: 'Column',
columnType: 'String',
},
description: {
type: 'Column',
columnType: 'String',
},
createdTime: {
type: 'Column',
columnType: 'String',
},
modifiedTime: {
type: 'Column',
columnType: 'String',
},
version: {
type: 'Column',
columnType: 'String',
},
thumbnailLink: {
type: 'Column',
columnType: 'String',
},
};

protected service: drive_v3.Drive;
protected records: RecordData[] = [];

constructor(datasource: DataSource, name: string, options: Options) {
super(name, datasource);

this.service = drive({ version: 'v3', auth: options.auth });

// https://developers.google.com/drive/api/reference/rest/v3/files
this.addFields(GoogleDriveCollection.schema);

// developers.google.com/drive/api/reference/rest/v3/files/create
this.addAction('Creates a copy of the file', { scope: 'Single', staticForm: true });

// filters/sort is supported
for (const schema of Object.values(this.schema.fields)) {
if (schema.type === 'Column') {
schema.filterOperators = GoogleDriveCollection.supportedOperators;
schema.isSortable = true;
}
}
}

async create(): Promise<RecordData[]> {
throw Error('Datasource missing create capability');
// const records = [];

// for (const datum of data) {
// await this.service.files.create();
// }

// return records;
}

async list(
caller: Caller,
filter: PaginatedFilter,
projection: Projection,
): Promise<RecordData[]> {
// https://developers.google.com/drive/api/guides/search-files#examples
const queryString = queryStringFromConditionTree(filter.conditionTree);

const res = await this.service.files.list({
q: queryString,
// fields: 'nextPageToken, files(id, name)',
spaces: 'drive', // appDataFolder? what is this
orderBy: filter.sort.map(s => (s.ascending ? s.field : `${s.field} desc`)).join(','),
pageSize: filter.page.limit,
// pageToken: 'placeholder-value', // TODO work on this
// Search won't work really poor circumvent will be to use a map to keep nextPageToken state
});

return projection.apply(res.data.files);
}

async update(): Promise<void> {
throw Error('Datasource missing update capability');
// See https://developers.google.com/drive/api/guides/manage-uploads
}

async delete(): Promise<void> {
throw Error('You cannot delete drive files from a Forest Admin datasource');
}

async aggregate(
caller: Caller,
filter: Filter,
aggregation: Aggregation,
limit?: number,
): Promise<AggregateResult[]> {
return aggregation.apply(
await this.list(caller, filter, aggregation.projection),
caller.timezone,
limit,
);
}
}
22 changes: 22 additions & 0 deletions packages/datasource-google-drive/src/datasource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { BaseDataSource, Logger } from '@forestadmin/datasource-toolkit';

import GoogleDriveCollection from './collection';

export type Options = {
name?: string;

/** GOOGLE API keys, defaults to process.env.GOOGLE_API_KEY. */
auth?: string;

/** Alternatively, you can specify the path to the service account credential file via */
// keyFile?: string;
};

export default class GoogleDriveDataSource extends BaseDataSource {
constructor(options: Options, logger: Logger) {
super();

logger?.('Debug', 'Building GoogleDriveCollection...');
this.addCollection(new GoogleDriveCollection(this, options.name ?? 'GoogleDrive', options));
}
}
7 changes: 7 additions & 0 deletions packages/datasource-google-drive/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { DataSourceFactory, Logger } from '@forestadmin/datasource-toolkit';

import GoogleDriveDataSource, { Options } from './datasource';

export default function createGoogleDriveDataSource(options: Options): DataSourceFactory {
return async (logger: Logger) => new GoogleDriveDataSource(options, logger);
}
76 changes: 76 additions & 0 deletions packages/datasource-google-drive/src/utils/query-converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
ConditionTree,
ConditionTreeBranch,
ConditionTreeLeaf,
Operator,
} from '@forestadmin/datasource-toolkit';

function makeWhereClause(field: string, operator: Operator, value?: unknown): string {
switch (operator) {
// Equality
case 'Equal':
return `${field} = '${value}'`;
case 'NotEqual':
return `${field} != '${value}'`;

case 'In':
return `'${value}' in ${field}`;
case 'NotIn':
return `not '${value}' in ${field}`;

// Orderable
case 'LessThan':
return `${field} < '${value}'`;
case 'GreaterThan':
return `${field} > '${value}'`;

// Strings
case 'Like':
return `${field} = '${value}'`;
case 'ILike':
return `${field} contains '${value}'`;
case 'NotContains':
return `not ${field} contains '${value}'`;

// How to handle this ? Only with search could be power full ? @RomainG
case 'Contains':
return `fullText contains '${value}'`;

default:
throw new Error(`Unsupported operator: "${operator}".`);
}
}

export default function queryStringFromConditionTree(conditionTree?: ConditionTree): string {
if (!conditionTree) return '';

let queryString = '';

if ((conditionTree as ConditionTreeBranch).aggregator !== undefined) {
const { aggregator, conditions } = conditionTree as ConditionTreeBranch;

if (aggregator === null) {
throw new Error('Invalid (null) aggregator.');
}

const operator = aggregator === 'And' ? 'and' : 'or';

if (!Array.isArray(conditions)) {
throw new Error('Conditions must be an array.');
}

queryString = conditions.reduce(
// MB other way around `(${queryStringFromConditionTree(condition)}) ${operator} ${acc}`
(acc, condition) => `${acc} ${operator} (${queryStringFromConditionTree(condition)})`,
queryString,
);
} else if ((conditionTree as ConditionTreeLeaf).operator !== undefined) {
const { field, operator, value } = conditionTree as ConditionTreeLeaf;

queryString += makeWhereClause(field, operator, value);
} else {
throw new Error('Invalid ConditionTree.');
}

return queryString;
}
7 changes: 7 additions & 0 deletions packages/datasource-google-drive/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src/**/*"]
}