mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
120 changed files with 9966 additions and 361 deletions
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
import XKnex, { Knex } from './lib/sql/CustomKnex'; |
||||
|
||||
// import XKnex, { Knex } from './lib/sql/CustomKnex';
|
||||
//
|
||||
export { DbFactory } from './lib/DbFactory'; |
||||
export { BaseModelSql } from './lib/sql/BaseModelSql'; |
||||
export { XKnex, Knex }; |
||||
// export { BaseModelSql } from './lib/sql/BaseModelSql';
|
||||
// export { XKnex, Knex };
|
||||
|
@ -1,153 +0,0 @@
|
||||
import uniqBy from 'lodash/uniqBy'; |
||||
import BaseRender from '../../BaseRender'; |
||||
import type { Acl } from '../../../../../../interface/config'; |
||||
|
||||
class ExpressXcMiddleware extends BaseRender { |
||||
/** |
||||
* |
||||
* @param dir |
||||
* @param filename |
||||
* @param ctx |
||||
* @param ctx.tn |
||||
* @param ctx.columns |
||||
* @param ctx.relations |
||||
*/ |
||||
constructor({ dir, filename, ctx }) { |
||||
super({ dir, filename, ctx }); |
||||
} |
||||
|
||||
/** |
||||
* Prepare variables used in code template |
||||
*/ |
||||
prepare() { |
||||
let data: any = {}; |
||||
|
||||
/* run of simple variable */ |
||||
data = this.ctx; |
||||
|
||||
/* for complex code provide a func and args - do derivation within the func cbk */ |
||||
data.hasMany = { |
||||
func: this._renderXcHasManyRoutePermissions.bind(this), |
||||
args: { |
||||
tn: this.ctx.tn, |
||||
columns: this.ctx.columns, |
||||
hasMany: this.ctx.hasMany, |
||||
relations: this.ctx.relations, |
||||
routeVersionLetter: this.ctx.routeVersionLetter, |
||||
}, |
||||
}; |
||||
|
||||
/* for complex code provide a func and args - do derivation within the func cbk */ |
||||
data.belongsTo = { |
||||
func: this._renderXcBelongsToRoutePermissions.bind(this), |
||||
args: { |
||||
dbType: this.ctx.dbType, |
||||
tn: this.ctx.tn, |
||||
columns: this.ctx.columns, |
||||
belongsTo: this.ctx.belongsTo, |
||||
relations: this.ctx.relations, |
||||
routeVersionLetter: this.ctx.routeVersionLetter, |
||||
}, |
||||
}; |
||||
|
||||
return data; |
||||
} |
||||
|
||||
private _renderXcHasManyRoutePermissions(args) { |
||||
let str = ''; |
||||
let hmRelations = args.relations |
||||
? args.relations.filter((r) => r.rtn === args.tn) |
||||
: []; |
||||
if (hmRelations.length > 1) |
||||
hmRelations = uniqBy(hmRelations, function (e) { |
||||
return [e.tn, e.rtn].join(); |
||||
}); |
||||
for (let i = 0; i < hmRelations.length; ++i) { |
||||
str += ` |
||||
'/api/${args.routeVersionLetter}1/${args.tn}/has/${hmRelations[i].tn}' : {get:{admin:true,user:true,guest:true}}, |
||||
'/api/${args.routeVersionLetter}1/${args.tn}/:parentId/${hmRelations[i].tn}' : {get:{admin:true,user:true,guest:true},post:{admin:true,user:true,guest:true}}, |
||||
'/api/${args.routeVersionLetter}1/${args.tn}/:parentId/${hmRelations[i].tn}/findOne' : {get:{admin:true,user:true,guest:true}}, |
||||
'/api/${args.routeVersionLetter}1/${args.tn}/:parentId/${hmRelations[i].tn}/count' : {get:{admin:true,user:true,guest:true}}, |
||||
'/api/${args.routeVersionLetter}1/${args.tn}/:parentId/${hmRelations[i].tn}/:id' : {get:{admin:true,user:true,guest:true},put:{admin:true,user:true,guest:true},delete:{admin:true,user:true,guest:true}}, |
||||
'/api/${args.routeVersionLetter}1/${args.tn}/:parentId/${hmRelations[i].tn}/:id/exists' : {get:{admin:true,user:true,guest:true}}, |
||||
`;
|
||||
} |
||||
|
||||
return str; |
||||
|
||||
/* iterate over has many relations */ |
||||
} |
||||
|
||||
_renderXcBelongsToRoutePermissions(args) { |
||||
let str = ''; |
||||
//
|
||||
let btRelations = args.relations |
||||
? args.relations.filter((r) => r.tn === args.tn) |
||||
: []; |
||||
if (btRelations.length > 1) |
||||
btRelations = uniqBy(btRelations, function (e) { |
||||
return [e.tn, e.rtn].join(); |
||||
}); |
||||
for (let i = 0; i < btRelations.length; ++i) { |
||||
str += `'/api/${args.routeVersionLetter}1/${args.tn}/belongs/:parents' : {get:{admin:true,user:true,guest:true}},`; |
||||
} |
||||
return str; |
||||
} |
||||
|
||||
getObject(): Acl { |
||||
return { |
||||
creator: { |
||||
read: true, |
||||
...(this.ctx.type !== 'view' |
||||
? { |
||||
create: true, |
||||
update: true, |
||||
delete: true, |
||||
} |
||||
: {}), |
||||
}, |
||||
editor: { |
||||
read: true, |
||||
...(this.ctx.type !== 'view' |
||||
? { |
||||
create: true, |
||||
update: true, |
||||
delete: true, |
||||
} |
||||
: {}), |
||||
}, |
||||
commenter: { |
||||
read: true, |
||||
...(this.ctx.type !== 'view' |
||||
? { |
||||
create: false, |
||||
update: false, |
||||
delete: false, |
||||
} |
||||
: {}), |
||||
}, |
||||
viewer: { |
||||
read: true, |
||||
...(this.ctx.type !== 'view' |
||||
? { |
||||
create: false, |
||||
update: false, |
||||
delete: false, |
||||
} |
||||
: {}), |
||||
}, |
||||
guest: { |
||||
read: false, |
||||
...(this.ctx.type !== 'view' |
||||
? { |
||||
create: false, |
||||
update: false, |
||||
delete: false, |
||||
} |
||||
: {}), |
||||
}, |
||||
}; |
||||
} |
||||
} |
||||
|
||||
export default ExpressXcMiddleware; |
@ -0,0 +1,61 @@
|
||||
import Ajv from 'ajv'; |
||||
import addFormats from 'ajv-formats'; |
||||
// @ts-ignore
|
||||
import swagger from '../../../../schema/swagger.json'; |
||||
import { NcError } from '../../helpers/catchError'; |
||||
import type { ErrorObject } from 'ajv'; |
||||
import type { NextFunction, Request, Response } from 'express'; |
||||
|
||||
export function parseHrtimeToSeconds(hrtime) { |
||||
const seconds = (hrtime[0] + hrtime[1] / 1e6).toFixed(3); |
||||
return seconds; |
||||
} |
||||
|
||||
const ajv = new Ajv({ strictSchema: false, strict: false }); // Initialize AJV
|
||||
|
||||
ajv.addSchema(swagger, 'swagger.json'); |
||||
addFormats(ajv); |
||||
|
||||
// A middleware generator to validate the request body
|
||||
export const getAjvValidatorMw = (schema) => { |
||||
return (req: Request, res: Response, next: NextFunction) => { |
||||
// Validate the request body against the schema
|
||||
const valid = ajv.validate( |
||||
typeof schema === 'string' ? { $ref: schema } : schema, |
||||
req.body |
||||
); |
||||
|
||||
// If the request body is valid, call the next middleware
|
||||
if (valid) { |
||||
next(); |
||||
} else { |
||||
const errors: ErrorObject[] | null | undefined = ajv.errors; |
||||
|
||||
// If the request body is invalid, send a response with an error message
|
||||
res.status(400).json({ |
||||
message: 'Invalid request body', |
||||
errors, |
||||
}); |
||||
} |
||||
}; |
||||
}; |
||||
|
||||
// a function to validate the payload against the schema
|
||||
export const validatePayload = (schema, payload) => { |
||||
// Validate the request body against the schema
|
||||
const valid = ajv.validate( |
||||
typeof schema === 'string' ? { $ref: schema } : schema, |
||||
payload |
||||
); |
||||
|
||||
// If the request body is not valid, throw error
|
||||
if (!valid) { |
||||
const errors: ErrorObject[] | null | undefined = ajv.errors; |
||||
|
||||
// If the request body is invalid, throw error with error message and errors
|
||||
NcError.ajvValidationError({ |
||||
message: 'Invalid request body', |
||||
errors, |
||||
}); |
||||
} |
||||
}; |
@ -0,0 +1,13 @@
|
||||
// return a middleware to set cache-control header
|
||||
// default period is 30 days
|
||||
export const getCacheMiddleware = (period: string | number = 2592000) => { |
||||
return async (req, res, next) => { |
||||
const { method } = req; |
||||
// only cache GET requests
|
||||
if (method === 'GET') { |
||||
// set cache-control header
|
||||
res.set('Cache-Control', `public, max-age=${period}`); |
||||
} |
||||
next(); |
||||
}; |
||||
}; |
@ -0,0 +1,208 @@
|
||||
import { customAlphabet } from 'nanoid'; |
||||
import { UITypes } from 'nocodb-sdk'; |
||||
import Column from '../../../models/Column'; |
||||
import { getUniqueColumnAliasName } from '../../helpers/getUniqueName'; |
||||
import validateParams from '../../helpers/validateParams'; |
||||
import type { |
||||
BoolType, |
||||
ColumnReqType, |
||||
LinkToAnotherRecordType, |
||||
LookupColumnReqType, |
||||
RelationTypes, |
||||
RollupColumnReqType, |
||||
TableType, |
||||
} from 'nocodb-sdk'; |
||||
import type LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn'; |
||||
import type LookupColumn from '../../../models/LookupColumn'; |
||||
import type Model from '../../../models/Model'; |
||||
|
||||
export const randomID = customAlphabet( |
||||
'1234567890abcdefghijklmnopqrstuvwxyz_', |
||||
10 |
||||
); |
||||
|
||||
export async function createHmAndBtColumn( |
||||
child: Model, |
||||
parent: Model, |
||||
childColumn: Column, |
||||
type?: RelationTypes, |
||||
alias?: string, |
||||
fkColName?: string, |
||||
virtual: BoolType = false, |
||||
isSystemCol = false |
||||
) { |
||||
// save bt column
|
||||
{ |
||||
const title = getUniqueColumnAliasName( |
||||
await child.getColumns(), |
||||
type === 'bt' ? alias : `${parent.title}` |
||||
); |
||||
await Column.insert<LinkToAnotherRecordColumn>({ |
||||
title, |
||||
|
||||
fk_model_id: child.id, |
||||
// ref_db_alias
|
||||
uidt: UITypes.LinkToAnotherRecord, |
||||
type: 'bt', |
||||
// db_type:
|
||||
|
||||
fk_child_column_id: childColumn.id, |
||||
fk_parent_column_id: parent.primaryKey.id, |
||||
fk_related_model_id: parent.id, |
||||
virtual, |
||||
system: isSystemCol, |
||||
fk_col_name: fkColName, |
||||
fk_index_name: fkColName, |
||||
}); |
||||
} |
||||
// save hm column
|
||||
{ |
||||
const title = getUniqueColumnAliasName( |
||||
await parent.getColumns(), |
||||
type === 'hm' ? alias : `${child.title} List` |
||||
); |
||||
await Column.insert({ |
||||
title, |
||||
fk_model_id: parent.id, |
||||
uidt: UITypes.LinkToAnotherRecord, |
||||
type: 'hm', |
||||
fk_child_column_id: childColumn.id, |
||||
fk_parent_column_id: parent.primaryKey.id, |
||||
fk_related_model_id: child.id, |
||||
virtual, |
||||
system: isSystemCol, |
||||
fk_col_name: fkColName, |
||||
fk_index_name: fkColName, |
||||
}); |
||||
} |
||||
} |
||||
|
||||
export async function validateRollupPayload(payload: ColumnReqType | Column) { |
||||
validateParams( |
||||
[ |
||||
'title', |
||||
'fk_relation_column_id', |
||||
'fk_rollup_column_id', |
||||
'rollup_function', |
||||
], |
||||
payload |
||||
); |
||||
|
||||
const relation = await ( |
||||
await Column.get({ |
||||
colId: (payload as RollupColumnReqType).fk_relation_column_id, |
||||
}) |
||||
).getColOptions<LinkToAnotherRecordType>(); |
||||
|
||||
if (!relation) { |
||||
throw new Error('Relation column not found'); |
||||
} |
||||
|
||||
let relatedColumn: Column; |
||||
switch (relation.type) { |
||||
case 'hm': |
||||
relatedColumn = await Column.get({ |
||||
colId: relation.fk_child_column_id, |
||||
}); |
||||
break; |
||||
case 'mm': |
||||
case 'bt': |
||||
relatedColumn = await Column.get({ |
||||
colId: relation.fk_parent_column_id, |
||||
}); |
||||
break; |
||||
} |
||||
|
||||
const relatedTable = await relatedColumn.getModel(); |
||||
if ( |
||||
!(await relatedTable.getColumns()).find( |
||||
(c) => c.id === (payload as RollupColumnReqType).fk_rollup_column_id |
||||
) |
||||
) |
||||
throw new Error('Rollup column not found in related table'); |
||||
} |
||||
|
||||
export async function validateLookupPayload( |
||||
payload: ColumnReqType, |
||||
columnId?: string |
||||
) { |
||||
validateParams( |
||||
['title', 'fk_relation_column_id', 'fk_lookup_column_id'], |
||||
payload |
||||
); |
||||
|
||||
// check for circular reference
|
||||
if (columnId) { |
||||
let lkCol: LookupColumn | LookupColumnReqType = |
||||
payload as LookupColumnReqType; |
||||
while (lkCol) { |
||||
// check if lookup column is same as column itself
|
||||
if (columnId === lkCol.fk_lookup_column_id) |
||||
throw new Error('Circular lookup reference not allowed'); |
||||
lkCol = await Column.get({ colId: lkCol.fk_lookup_column_id }).then( |
||||
(c: Column) => { |
||||
if (c.uidt === 'Lookup') { |
||||
return c.getColOptions<LookupColumn>(); |
||||
} |
||||
return null; |
||||
} |
||||
); |
||||
} |
||||
} |
||||
|
||||
const relation = await ( |
||||
await Column.get({ |
||||
colId: (payload as LookupColumnReqType).fk_relation_column_id, |
||||
}) |
||||
).getColOptions<LinkToAnotherRecordType>(); |
||||
|
||||
if (!relation) { |
||||
throw new Error('Relation column not found'); |
||||
} |
||||
|
||||
let relatedColumn: Column; |
||||
switch (relation.type) { |
||||
case 'hm': |
||||
relatedColumn = await Column.get({ |
||||
colId: relation.fk_child_column_id, |
||||
}); |
||||
break; |
||||
case 'mm': |
||||
case 'bt': |
||||
relatedColumn = await Column.get({ |
||||
colId: relation.fk_parent_column_id, |
||||
}); |
||||
break; |
||||
} |
||||
|
||||
const relatedTable = await relatedColumn.getModel(); |
||||
if ( |
||||
!(await relatedTable.getColumns()).find( |
||||
(c) => c.id === (payload as LookupColumnReqType).fk_lookup_column_id |
||||
) |
||||
) |
||||
throw new Error('Lookup column not found in related table'); |
||||
} |
||||
|
||||
export const validateRequiredField = ( |
||||
payload: Record<string, any>, |
||||
requiredProps: string[] |
||||
) => { |
||||
return requiredProps.every( |
||||
(prop) => |
||||
prop in payload && payload[prop] !== undefined && payload[prop] !== null |
||||
); |
||||
}; |
||||
|
||||
// generate unique foreign key constraint name for foreign key
|
||||
export const generateFkName = (parent: TableType, child: TableType) => { |
||||
// generate a unique constraint name by taking first 10 chars of parent and child table name (by replacing all non word chars with _)
|
||||
// and appending a random string of 15 chars maximum length.
|
||||
// In database constraint name can be upto 64 chars and here we are generating a name of maximum 40 chars
|
||||
const constraintName = `fk_${parent.table_name |
||||
.replace(/\W+/g, '_') |
||||
.slice(0, 10)}_${child.table_name |
||||
.replace(/\W+/g, '_') |
||||
.slice(0, 10)}_${randomID(15)}`;
|
||||
return constraintName; |
||||
}; |
@ -0,0 +1,137 @@
|
||||
export function convertUnits( |
||||
unit: string, |
||||
type: 'mysql' | 'mssql' | 'pg' | 'sqlite' |
||||
) { |
||||
switch (unit) { |
||||
case 'milliseconds': |
||||
case 'ms': { |
||||
switch (type) { |
||||
case 'mssql': |
||||
return 'millisecond'; |
||||
case 'mysql': |
||||
// MySQL doesn't support millisecond
|
||||
// hence change from MICROSECOND to millisecond manually
|
||||
return 'MICROSECOND'; |
||||
case 'pg': |
||||
case 'sqlite': |
||||
return 'milliseconds'; |
||||
default: |
||||
return unit; |
||||
} |
||||
} |
||||
case 'seconds': |
||||
case 's': { |
||||
switch (type) { |
||||
case 'mssql': |
||||
case 'pg': |
||||
return 'second'; |
||||
case 'mysql': |
||||
return 'SECOND'; |
||||
case 'sqlite': |
||||
return 'seconds'; |
||||
default: |
||||
return unit; |
||||
} |
||||
} |
||||
case 'minutes': |
||||
case 'm': { |
||||
switch (type) { |
||||
case 'mssql': |
||||
case 'pg': |
||||
return 'minute'; |
||||
case 'mysql': |
||||
return 'MINUTE'; |
||||
case 'sqlite': |
||||
return 'minutes'; |
||||
default: |
||||
return unit; |
||||
} |
||||
} |
||||
case 'hours': |
||||
case 'h': { |
||||
switch (type) { |
||||
case 'mssql': |
||||
case 'pg': |
||||
return 'hour'; |
||||
case 'mysql': |
||||
return 'HOUR'; |
||||
case 'sqlite': |
||||
return 'hours'; |
||||
default: |
||||
return unit; |
||||
} |
||||
} |
||||
case 'days': |
||||
case 'd': { |
||||
switch (type) { |
||||
case 'mssql': |
||||
case 'pg': |
||||
return 'day'; |
||||
case 'mysql': |
||||
return 'DAY'; |
||||
case 'sqlite': |
||||
return 'days'; |
||||
default: |
||||
return unit; |
||||
} |
||||
} |
||||
case 'weeks': |
||||
case 'w': { |
||||
switch (type) { |
||||
case 'mssql': |
||||
case 'pg': |
||||
return 'week'; |
||||
case 'mysql': |
||||
return 'WEEK'; |
||||
case 'sqlite': |
||||
return 'weeks'; |
||||
default: |
||||
return unit; |
||||
} |
||||
} |
||||
case 'months': |
||||
case 'M': { |
||||
switch (type) { |
||||
case 'mssql': |
||||
case 'pg': |
||||
return 'month'; |
||||
case 'mysql': |
||||
return 'MONTH'; |
||||
case 'sqlite': |
||||
return 'months'; |
||||
default: |
||||
return unit; |
||||
} |
||||
} |
||||
case 'quarters': |
||||
case 'Q': { |
||||
switch (type) { |
||||
case 'mssql': |
||||
case 'pg': |
||||
return 'quarter'; |
||||
case 'mysql': |
||||
return 'QUARTER'; |
||||
case 'sqlite': |
||||
return 'quarters'; |
||||
default: |
||||
return unit; |
||||
} |
||||
} |
||||
case 'years': |
||||
case 'y': { |
||||
switch (type) { |
||||
case 'mssql': |
||||
case 'pg': |
||||
return 'year'; |
||||
case 'mysql': |
||||
return 'YEAR'; |
||||
case 'sqlite': |
||||
return 'years'; |
||||
default: |
||||
return unit; |
||||
} |
||||
} |
||||
default: |
||||
return unit; |
||||
} |
||||
} |
@ -0,0 +1,54 @@
|
||||
import dayjs from 'dayjs'; |
||||
|
||||
// todo: tobe fixed
|
||||
// import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
// extend(customParseFormat);
|
||||
|
||||
export function getWeekdayByText(v: string) { |
||||
return { |
||||
monday: 0, |
||||
tuesday: 1, |
||||
wednesday: 2, |
||||
thursday: 3, |
||||
friday: 4, |
||||
saturday: 5, |
||||
sunday: 6, |
||||
}[v?.toLowerCase() || 'monday']; |
||||
} |
||||
|
||||
export function getWeekdayByIndex(idx: number): string { |
||||
return { |
||||
0: 'monday', |
||||
1: 'tuesday', |
||||
2: 'wednesday', |
||||
3: 'thursday', |
||||
4: 'friday', |
||||
5: 'saturday', |
||||
6: 'sunday', |
||||
}[idx || 0]; |
||||
} |
||||
|
||||
export function validateDateWithUnknownFormat(v: string) { |
||||
const dateFormats = [ |
||||
'DD-MM-YYYY', |
||||
'MM-DD-YYYY', |
||||
'YYYY-MM-DD', |
||||
'DD/MM/YYYY', |
||||
'MM/DD/YYYY', |
||||
'YYYY/MM/DD', |
||||
'DD MM YYYY', |
||||
'MM DD YYYY', |
||||
'YYYY MM DD', |
||||
]; |
||||
for (const format of dateFormats) { |
||||
if (dayjs(v, format, true).isValid() as any) { |
||||
return true; |
||||
} |
||||
for (const timeFormat of ['HH:mm', 'HH:mm:ss', 'HH:mm:ss.SSS']) { |
||||
if (dayjs(v, `${format} ${timeFormat}`, true).isValid() as any) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
return false; |
||||
} |
@ -0,0 +1,216 @@
|
||||
import { isSystemColumn, RelationTypes, UITypes } from 'nocodb-sdk'; |
||||
import { View } from '../models'; |
||||
import type { |
||||
Column, |
||||
LinkToAnotherRecordColumn, |
||||
LookupColumn, |
||||
Model, |
||||
} from '../models'; |
||||
|
||||
const getAst = async ({ |
||||
query, |
||||
extractOnlyPrimaries = false, |
||||
includePkByDefault = true, |
||||
model, |
||||
view, |
||||
dependencyFields = { |
||||
...(query || {}), |
||||
nested: { ...(query?.nested || {}) }, |
||||
fieldsSet: new Set(), |
||||
}, |
||||
}: { |
||||
query?: RequestQuery; |
||||
extractOnlyPrimaries?: boolean; |
||||
includePkByDefault?: boolean; |
||||
model: Model; |
||||
view?: View; |
||||
dependencyFields?: DependantFields; |
||||
}) => { |
||||
// set default values of dependencyFields and nested
|
||||
dependencyFields.nested = dependencyFields.nested || {}; |
||||
dependencyFields.fieldsSet = dependencyFields.fieldsSet || new Set(); |
||||
|
||||
if (!model.columns?.length) await model.getColumns(); |
||||
|
||||
// extract only pk and pv
|
||||
if (extractOnlyPrimaries) { |
||||
const ast = { |
||||
...(model.primaryKeys |
||||
? model.primaryKeys.reduce((o, pk) => ({ ...o, [pk.title]: 1 }), {}) |
||||
: {}), |
||||
...(model.displayValue ? { [model.displayValue.title]: 1 } : {}), |
||||
}; |
||||
await Promise.all( |
||||
model.primaryKeys.map((c) => extractDependencies(c, dependencyFields)) |
||||
); |
||||
|
||||
await extractDependencies(model.displayValue, dependencyFields); |
||||
|
||||
return { ast, dependencyFields }; |
||||
} |
||||
|
||||
let fields = query?.fields || query?.f; |
||||
if (fields && fields !== '*') { |
||||
fields = Array.isArray(fields) ? fields : fields.split(','); |
||||
} else { |
||||
fields = null; |
||||
} |
||||
|
||||
let allowedCols = null; |
||||
if (view) |
||||
allowedCols = (await View.getColumns(view.id)).reduce( |
||||
(o, c) => ({ |
||||
...o, |
||||
[c.fk_column_id]: c.show, |
||||
}), |
||||
{} |
||||
); |
||||
|
||||
const ast = await model.columns.reduce(async (obj, col: Column) => { |
||||
let value: number | boolean | { [key: string]: any } = 1; |
||||
const nestedFields = |
||||
query?.nested?.[col.title]?.fields || query?.nested?.[col.title]?.f; |
||||
if (nestedFields && nestedFields !== '*') { |
||||
if (col.uidt === UITypes.LinkToAnotherRecord) { |
||||
const model = await col |
||||
.getColOptions<LinkToAnotherRecordColumn>() |
||||
.then((colOpt) => colOpt.getRelatedTable()); |
||||
|
||||
const { ast } = await getAst({ |
||||
model, |
||||
query: query?.nested?.[col.title], |
||||
dependencyFields: (dependencyFields.nested[col.title] = |
||||
dependencyFields.nested[col.title] || { |
||||
nested: {}, |
||||
fieldsSet: new Set(), |
||||
}), |
||||
}); |
||||
|
||||
value = ast; |
||||
|
||||
// todo: include field relative to the relation => pk / fk
|
||||
} else { |
||||
value = (Array.isArray(fields) ? fields : fields.split(',')).reduce( |
||||
(o, f) => ({ ...o, [f]: 1 }), |
||||
{} |
||||
); |
||||
} |
||||
} else if (col.uidt === UITypes.LinkToAnotherRecord) { |
||||
const model = await col |
||||
.getColOptions<LinkToAnotherRecordColumn>() |
||||
.then((colOpt) => colOpt.getRelatedTable()); |
||||
|
||||
value = ( |
||||
await getAst({ |
||||
model, |
||||
query: query?.nested?.[col.title], |
||||
extractOnlyPrimaries: nestedFields !== '*', |
||||
dependencyFields: (dependencyFields.nested[col.title] = |
||||
dependencyFields.nested[col.title] || { |
||||
nested: {}, |
||||
fieldsSet: new Set(), |
||||
}), |
||||
}) |
||||
).ast; |
||||
} |
||||
|
||||
const isRequested = |
||||
allowedCols && (!includePkByDefault || !col.pk) |
||||
? allowedCols[col.id] && |
||||
(!isSystemColumn(col) || view.show_system_fields) && |
||||
(!fields?.length || fields.includes(col.title)) && |
||||
value |
||||
: fields?.length |
||||
? fields.includes(col.title) && value |
||||
: value; |
||||
if (isRequested || col.pk) await extractDependencies(col, dependencyFields); |
||||
|
||||
return { |
||||
...(await obj), |
||||
[col.title]: isRequested, |
||||
}; |
||||
}, Promise.resolve({})); |
||||
|
||||
return { ast, dependencyFields }; |
||||
}; |
||||
|
||||
const extractDependencies = async ( |
||||
column: Column, |
||||
dependencyFields: DependantFields = { |
||||
nested: {}, |
||||
fieldsSet: new Set(), |
||||
} |
||||
) => { |
||||
switch (column.uidt) { |
||||
case UITypes.Lookup: |
||||
await extractLookupDependencies(column, dependencyFields); |
||||
break; |
||||
case UITypes.LinkToAnotherRecord: |
||||
await extractRelationDependencies(column, dependencyFields); |
||||
break; |
||||
default: |
||||
dependencyFields.fieldsSet.add(column.title); |
||||
break; |
||||
} |
||||
}; |
||||
|
||||
const extractLookupDependencies = async ( |
||||
lookUpColumn: Column<LookupColumn>, |
||||
dependencyFields: DependantFields = { |
||||
nested: {}, |
||||
fieldsSet: new Set(), |
||||
} |
||||
) => { |
||||
const lookupColumnOpts = await lookUpColumn.getColOptions(); |
||||
const relationColumn = await lookupColumnOpts.getRelationColumn(); |
||||
await extractRelationDependencies(relationColumn, dependencyFields); |
||||
await extractDependencies( |
||||
await lookupColumnOpts.getLookupColumn(), |
||||
(dependencyFields.nested[relationColumn.title] = dependencyFields.nested[ |
||||
relationColumn.title |
||||
] || { |
||||
nested: {}, |
||||
fieldsSet: new Set(), |
||||
}) |
||||
); |
||||
}; |
||||
|
||||
const extractRelationDependencies = async ( |
||||
relationColumn: Column<LinkToAnotherRecordColumn>, |
||||
dependencyFields: DependantFields = { |
||||
nested: {}, |
||||
fieldsSet: new Set(), |
||||
} |
||||
) => { |
||||
const relationColumnOpts = await relationColumn.getColOptions(); |
||||
|
||||
switch (relationColumnOpts.type) { |
||||
case RelationTypes.HAS_MANY: |
||||
dependencyFields.fieldsSet.add( |
||||
await relationColumnOpts.getParentColumn().then((col) => col.title) |
||||
); |
||||
break; |
||||
case RelationTypes.BELONGS_TO: |
||||
case RelationTypes.MANY_TO_MANY: |
||||
dependencyFields.fieldsSet.add( |
||||
await relationColumnOpts.getChildColumn().then((col) => col.title) |
||||
); |
||||
|
||||
break; |
||||
} |
||||
}; |
||||
|
||||
type RequestQuery = { |
||||
[fields in 'f' | 'fields']?: string | string[]; |
||||
} & { |
||||
nested?: { |
||||
[field: string]: RequestQuery; |
||||
}; |
||||
}; |
||||
|
||||
interface DependantFields { |
||||
fieldsSet?: Set<string>; |
||||
nested?: DependantFields; |
||||
} |
||||
|
||||
export default getAst; |
@ -0,0 +1,6 @@
|
||||
import { populateMeta } from './populateMeta'; |
||||
export * from './columnHelpers'; |
||||
export * from './apiHelpers'; |
||||
export * from './cacheHelpers'; |
||||
|
||||
export { populateMeta }; |
@ -0,0 +1,280 @@
|
||||
import { ModelTypes, UITypes, ViewTypes } from 'nocodb-sdk'; |
||||
import Column from '../../../models/Column'; |
||||
import Model from '../../../models/Model'; |
||||
import NcHelp from '../../../utils/NcHelp'; |
||||
import View from '../../../models/View'; |
||||
import NcConnectionMgrv2 from '../../../utils/common/NcConnectionMgrv2'; |
||||
import getTableNameAlias, { |
||||
getColumnNameAlias, |
||||
} from '../../helpers/getTableName'; |
||||
import getColumnUiType from '../../helpers/getColumnUiType'; |
||||
import mapDefaultDisplayValue from '../../helpers/mapDefaultDisplayValue'; |
||||
import { extractAndGenerateManyToManyRelations } from '../../../services/metaDiff.svc'; |
||||
import { IGNORE_TABLES } from '../../../utils/common/BaseApiBuilder'; |
||||
import type LinkToAnotherRecordColumn from '../../../models/LinkToAnotherRecordColumn'; |
||||
import type Base from '../../../models/Base'; |
||||
import type Project from '../../../models/Project'; |
||||
|
||||
export async function populateMeta(base: Base, project: Project): Promise<any> { |
||||
const info = { |
||||
type: 'rest', |
||||
apiCount: 0, |
||||
tablesCount: 0, |
||||
relationsCount: 0, |
||||
viewsCount: 0, |
||||
client: (await base?.getConnectionConfig())?.client, |
||||
timeTaken: 0, |
||||
}; |
||||
|
||||
const t = process.hrtime(); |
||||
const sqlClient = await NcConnectionMgrv2.getSqlClient(base); |
||||
let order = 1; |
||||
const models2: { [tableName: string]: Model } = {}; |
||||
|
||||
const virtualColumnsInsert = []; |
||||
|
||||
/* Get all relations */ |
||||
const relations = (await sqlClient.relationListAll())?.data?.list; |
||||
|
||||
info.relationsCount = relations.length; |
||||
|
||||
let tables = (await sqlClient.tableList())?.data?.list |
||||
?.filter(({ tn }) => !IGNORE_TABLES.includes(tn)) |
||||
?.map((t) => { |
||||
t.order = ++order; |
||||
t.title = getTableNameAlias(t.tn, project.prefix, base); |
||||
t.table_name = t.tn; |
||||
return t; |
||||
}); |
||||
|
||||
/* filter based on prefix */ |
||||
if (base.is_meta && project?.prefix) { |
||||
tables = tables.filter((t) => { |
||||
return t?.tn?.startsWith(project?.prefix); |
||||
}); |
||||
} |
||||
|
||||
info.tablesCount = tables.length; |
||||
|
||||
tables.forEach((t) => { |
||||
t.title = getTableNameAlias(t.tn, project.prefix, base); |
||||
}); |
||||
|
||||
relations.forEach((r) => { |
||||
r.title = getTableNameAlias(r.tn, project.prefix, base); |
||||
r.rtitle = getTableNameAlias(r.rtn, project.prefix, base); |
||||
}); |
||||
|
||||
// await this.syncRelations();
|
||||
|
||||
const tableMetasInsert = tables.map((table) => { |
||||
return async () => { |
||||
/* filter relation where this table is present */ |
||||
const tableRelations = relations.filter( |
||||
(r) => r.tn === table.tn || r.rtn === table.tn |
||||
); |
||||
|
||||
const columns: Array< |
||||
Omit<Column, 'column_name' | 'title'> & { |
||||
cn: string; |
||||
system?: boolean; |
||||
} |
||||
> = (await sqlClient.columnList({ tn: table.tn }))?.data?.list; |
||||
|
||||
const hasMany = |
||||
table.type === 'view' |
||||
? [] |
||||
: tableRelations.filter((r) => r.rtn === table.tn); |
||||
const belongsTo = |
||||
table.type === 'view' |
||||
? [] |
||||
: tableRelations.filter((r) => r.tn === table.tn); |
||||
|
||||
mapDefaultDisplayValue(columns); |
||||
|
||||
// add vitual columns
|
||||
const virtualColumns = [ |
||||
...hasMany.map((hm) => { |
||||
return { |
||||
uidt: UITypes.LinkToAnotherRecord, |
||||
type: 'hm', |
||||
hm, |
||||
title: `${hm.title} List`, |
||||
}; |
||||
}), |
||||
...belongsTo.map((bt) => { |
||||
// find and mark foreign key column
|
||||
const fkColumn = columns.find((c) => c.cn === bt.cn); |
||||
if (fkColumn) { |
||||
fkColumn.uidt = UITypes.ForeignKey; |
||||
fkColumn.system = true; |
||||
} |
||||
|
||||
return { |
||||
uidt: UITypes.LinkToAnotherRecord, |
||||
type: 'bt', |
||||
bt, |
||||
title: `${bt.rtitle}`, |
||||
}; |
||||
}), |
||||
]; |
||||
|
||||
// await Model.insert(project.id, base.id, meta);
|
||||
|
||||
/* create nc_models and its rows if it doesn't exists */ |
||||
models2[table.table_name] = await Model.insert(project.id, base.id, { |
||||
table_name: table.tn || table.table_name, |
||||
title: table.title, |
||||
type: table.type || 'table', |
||||
order: table.order, |
||||
}); |
||||
|
||||
// table crud apis
|
||||
info.apiCount += 5; |
||||
|
||||
let colOrder = 1; |
||||
|
||||
for (const column of columns) { |
||||
await Column.insert({ |
||||
uidt: column.uidt || getColumnUiType(base, column), |
||||
fk_model_id: models2[table.tn].id, |
||||
...column, |
||||
title: getColumnNameAlias(column.cn, base), |
||||
column_name: column.cn, |
||||
order: colOrder++, |
||||
}); |
||||
} |
||||
virtualColumnsInsert.push(async () => { |
||||
const columnNames = {}; |
||||
for (const column of virtualColumns) { |
||||
// generate unique name if there is any duplicate column name
|
||||
let c = 0; |
||||
while (`${column.title}${c || ''}` in columnNames) { |
||||
c++; |
||||
} |
||||
column.title = `${column.title}${c || ''}`; |
||||
columnNames[column.title] = true; |
||||
|
||||
const rel = column.hm || column.bt; |
||||
|
||||
const rel_column_id = (await models2?.[rel.tn]?.getColumns())?.find( |
||||
(c) => c.column_name === rel.cn |
||||
)?.id; |
||||
|
||||
const tnId = models2?.[rel.tn]?.id; |
||||
|
||||
const ref_rel_column_id = ( |
||||
await models2?.[rel.rtn]?.getColumns() |
||||
)?.find((c) => c.column_name === rel.rcn)?.id; |
||||
|
||||
const rtnId = models2?.[rel.rtn]?.id; |
||||
|
||||
try { |
||||
await Column.insert<LinkToAnotherRecordColumn>({ |
||||
project_id: project.id, |
||||
db_alias: base.id, |
||||
fk_model_id: models2[table.tn].id, |
||||
cn: column.cn, |
||||
title: column.title, |
||||
uidt: column.uidt, |
||||
type: column.hm ? 'hm' : column.mm ? 'mm' : 'bt', |
||||
// column_id,
|
||||
fk_child_column_id: rel_column_id, |
||||
fk_parent_column_id: ref_rel_column_id, |
||||
fk_index_name: rel.cstn, |
||||
ur: rel.ur, |
||||
dr: rel.dr, |
||||
order: colOrder++, |
||||
fk_related_model_id: column.hm ? tnId : rtnId, |
||||
system: column.system, |
||||
}); |
||||
|
||||
// nested relations data apis
|
||||
info.apiCount += 5; |
||||
} catch (e) { |
||||
console.log(e); |
||||
} |
||||
} |
||||
}); |
||||
}; |
||||
}); |
||||
|
||||
/* handle xc_tables update in parallel */ |
||||
await NcHelp.executeOperations(tableMetasInsert, base.type); |
||||
await NcHelp.executeOperations(virtualColumnsInsert, base.type); |
||||
await extractAndGenerateManyToManyRelations(Object.values(models2)); |
||||
|
||||
let views: Array<{ order: number; table_name: string; title: string }> = ( |
||||
await sqlClient.viewList() |
||||
)?.data?.list |
||||
// ?.filter(({ tn }) => !IGNORE_TABLES.includes(tn))
|
||||
?.map((v) => { |
||||
v.order = ++order; |
||||
v.table_name = v.view_name; |
||||
v.title = getTableNameAlias(v.view_name, project.prefix, base); |
||||
return v; |
||||
}); |
||||
|
||||
/* filter based on prefix */ |
||||
if (base.is_meta && project?.prefix) { |
||||
views = tables.filter((t) => { |
||||
return t?.tn?.startsWith(project?.prefix); |
||||
}); |
||||
} |
||||
|
||||
info.viewsCount = views.length; |
||||
|
||||
const viewMetasInsert = views.map((table) => { |
||||
return async () => { |
||||
const columns = (await sqlClient.columnList({ tn: table.table_name })) |
||||
?.data?.list; |
||||
|
||||
mapDefaultDisplayValue(columns); |
||||
|
||||
/* create nc_models and its rows if it doesn't exists */ |
||||
models2[table.table_name] = await Model.insert(project.id, base.id, { |
||||
table_name: table.table_name, |
||||
title: getTableNameAlias(table.table_name, project.prefix, base), |
||||
// todo: sanitize
|
||||
type: ModelTypes.VIEW, |
||||
order: table.order, |
||||
}); |
||||
|
||||
let colOrder = 1; |
||||
|
||||
// view apis
|
||||
info.apiCount += 2; |
||||
|
||||
for (const column of columns) { |
||||
await Column.insert({ |
||||
fk_model_id: models2[table.table_name].id, |
||||
...column, |
||||
title: getColumnNameAlias(column.cn, base), |
||||
order: colOrder++, |
||||
uidt: getColumnUiType(base, column), |
||||
}); |
||||
} |
||||
}; |
||||
}); |
||||
|
||||
await NcHelp.executeOperations(viewMetasInsert, base.type); |
||||
|
||||
// fix pv column for created grid views
|
||||
const models = await Model.list({ project_id: project.id, base_id: base.id }); |
||||
|
||||
for (const model of models) { |
||||
const views = await model.getViews(); |
||||
for (const view of views) { |
||||
if (view.type === ViewTypes.GRID) { |
||||
await View.fixPVColumnForView(view.id); |
||||
} |
||||
} |
||||
} |
||||
|
||||
const t1 = process.hrtime(t); |
||||
const t2 = t1[0] + t1[1] / 1000000000; |
||||
|
||||
(info as any).timeTaken = t2.toFixed(1); |
||||
|
||||
return info; |
||||
} |
@ -0,0 +1,11 @@
|
||||
export function sanitize(v) { |
||||
if (typeof v !== 'string') return v; |
||||
return v?.replace(/([^\\]|^)(\?+)/g, (_, m1, m2) => { |
||||
return `${m1}${m2.split('?').join('\\?')}`; |
||||
}); |
||||
} |
||||
|
||||
export function unsanitize(v) { |
||||
if (typeof v !== 'string') return v; |
||||
return v?.replace(/\\[?]/g, '?'); |
||||
} |
@ -0,0 +1,15 @@
|
||||
export default interface IEmailAdapter { |
||||
init(): Promise<any>; |
||||
mailSend(mail: XcEmail): Promise<any>; |
||||
test(email): Promise<boolean>; |
||||
} |
||||
|
||||
interface XcEmail { |
||||
// from?:string;
|
||||
to: string; |
||||
subject: string; |
||||
html?: string; |
||||
text?: string; |
||||
} |
||||
|
||||
export { XcEmail }; |
@ -0,0 +1,17 @@
|
||||
export default interface IStorageAdapter { |
||||
init(): Promise<any>; |
||||
fileCreate(destPath: string, file: XcFile): Promise<any>; |
||||
fileDelete(filePath: string): Promise<any>; |
||||
fileRead(filePath: string): Promise<any>; |
||||
test(): Promise<boolean>; |
||||
} |
||||
|
||||
interface XcFile { |
||||
originalname: string; |
||||
path: string; |
||||
mimetype: string; |
||||
size: number | string; |
||||
buffer?: any; |
||||
} |
||||
|
||||
export { XcFile }; |
@ -0,0 +1,8 @@
|
||||
export default interface XcDynamicChanges { |
||||
onTableCreate(tn: string): Promise<void>; |
||||
onTableUpdate(changeObj: any): Promise<void>; |
||||
onTableDelete(tn: string): Promise<void>; |
||||
onTableRename(oldTableName: string, newTableName: string): Promise<void>; |
||||
onHandlerCodeUpdate(tn: string): Promise<void>; |
||||
onMetaUpdate(tn: string): Promise<void>; |
||||
} |
@ -0,0 +1,2 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export default interface XcMetaMgr {} |
@ -0,0 +1,293 @@
|
||||
import type { Handler } from 'express'; |
||||
import type * as e from 'express'; |
||||
import type { Knex } from 'knex'; |
||||
|
||||
export interface Route { |
||||
path: string; |
||||
type: RouteType | string; |
||||
handler: Array<Handler | string>; |
||||
acl: { |
||||
[key: string]: boolean; |
||||
}; |
||||
disabled?: boolean; |
||||
functions?: string[]; |
||||
} |
||||
|
||||
export enum RouteType { |
||||
GET = 'get', |
||||
POST = 'post', |
||||
PUT = 'put', |
||||
PATCH = 'patch', |
||||
DELETE = 'delete', |
||||
HEAD = 'head', |
||||
OPTIONS = 'options', |
||||
} |
||||
|
||||
type InflectionTypes = |
||||
| 'pluralize' |
||||
| 'singularize' |
||||
| 'inflect' |
||||
| 'camelize' |
||||
| 'underscore' |
||||
| 'humanize' |
||||
| 'capitalize' |
||||
| 'dasherize' |
||||
| 'titleize' |
||||
| 'demodulize' |
||||
| 'tableize' |
||||
| 'classify' |
||||
| 'foreign_key' |
||||
| 'ordinalize' |
||||
| 'transform' |
||||
| 'none'; |
||||
|
||||
export interface DbConfig extends Knex.Config { |
||||
client: string; |
||||
|
||||
connection: Knex.StaticConnectionConfig | Knex.Config | any; |
||||
|
||||
meta: { |
||||
dbAlias: string; |
||||
|
||||
metaTables?: 'db' | 'file'; |
||||
tn?: string; |
||||
models?: { |
||||
disabled: boolean; |
||||
}; |
||||
|
||||
routes?: { |
||||
disabled: boolean; |
||||
}; |
||||
|
||||
hooks?: { |
||||
disabled: boolean; |
||||
}; |
||||
|
||||
migrations?: { |
||||
disabled: boolean; |
||||
name: 'nc_evolutions'; |
||||
}; |
||||
|
||||
api: { |
||||
type: 'rest' | 'graphql' | 'grpc'; |
||||
prefix: string; |
||||
swagger?: boolean; |
||||
graphiql?: boolean; |
||||
graphqlDepthLimit?: number; |
||||
}; |
||||
|
||||
allSchemas?: boolean; |
||||
|
||||
ignoreTables?: string[]; |
||||
readonly?: boolean; |
||||
|
||||
query?: { |
||||
print?: boolean; |
||||
explain?: boolean; |
||||
measure?: boolean; |
||||
}; |
||||
reset?: boolean; |
||||
dbtype?: 'vitess' | string; |
||||
pluralize?: boolean; |
||||
inflection?: { |
||||
tn?: InflectionTypes; |
||||
cn?: InflectionTypes; |
||||
}; |
||||
}; |
||||
} |
||||
|
||||
// Refer : https://www.npmjs.com/package/jsonwebtoken
|
||||
interface JwtOptions { |
||||
algorithm?: string; |
||||
expiresIn?: string | number; |
||||
notBefore?: string | number; |
||||
audience?: string; |
||||
issuer?: string; |
||||
jwtid?: any; |
||||
subject?: string; |
||||
noTimestamp?: any; |
||||
header?: any; |
||||
keyid?: any; |
||||
} |
||||
|
||||
export interface AuthConfig { |
||||
jwt?: { |
||||
secret: string; |
||||
[key: string]: any; |
||||
dbAlias?: string; |
||||
options?: JwtOptions; |
||||
}; |
||||
masterKey?: { |
||||
secret: string; |
||||
}; |
||||
middleware?: { |
||||
url: string; |
||||
}; |
||||
disabled?: boolean; |
||||
} |
||||
|
||||
export interface MiddlewareConfig { |
||||
handler?: (...args: any[]) => any; |
||||
} |
||||
|
||||
export interface ACLConfig { |
||||
roles?: string[]; |
||||
defaultRoles?: string[]; |
||||
} |
||||
|
||||
export interface MailerConfig { |
||||
[key: string]: any; |
||||
} |
||||
|
||||
export interface ServerlessConfig { |
||||
aws?: { |
||||
lambda: boolean; |
||||
}; |
||||
gcp?: { |
||||
cloudFunction: boolean; |
||||
}; |
||||
azure?: { |
||||
cloudFunctionApp: boolean; |
||||
}; |
||||
zeit?: { |
||||
now: boolean; |
||||
}; |
||||
alibaba?: { |
||||
functionCompute: boolean; |
||||
}; |
||||
serverlessFramework?: { |
||||
http: boolean; |
||||
}; |
||||
} |
||||
|
||||
export interface NcGui { |
||||
path?: string; |
||||
disabled?: boolean; |
||||
favicon?: string; |
||||
logo?: string; |
||||
} |
||||
|
||||
// @ts-ignore
|
||||
export interface NcConfig { |
||||
title?: string; |
||||
version?: string; |
||||
|
||||
envs: { |
||||
[key: string]: { |
||||
db: DbConfig[]; |
||||
api?: any; |
||||
publicUrl?: string; |
||||
}; |
||||
}; |
||||
|
||||
// dbs: DbConfig[];
|
||||
|
||||
auth?: AuthConfig; |
||||
middleware?: MiddlewareConfig[]; |
||||
acl?: ACLConfig; |
||||
port?: number; |
||||
host?: string; |
||||
cluster?: number; |
||||
|
||||
mailer?: MailerConfig; |
||||
make?: () => NcConfig; |
||||
serverless?: ServerlessConfig; |
||||
|
||||
toolDir?: string; |
||||
env?: 'production' | 'dev' | 'test' | string; |
||||
workingEnv?: string; |
||||
|
||||
seedsFolder?: string | string[]; |
||||
queriesFolder?: string | string[]; |
||||
apisFolder?: string | string[]; |
||||
projectType?: 'rest' | 'graphql' | 'grpc'; |
||||
type?: 'mvc' | 'package' | 'docker'; |
||||
language?: 'ts' | 'js'; |
||||
meta?: { |
||||
db?: any; |
||||
}; |
||||
api?: any; |
||||
gui?: NcGui; |
||||
try?: boolean; |
||||
|
||||
dashboardPath?: string; |
||||
|
||||
prefix?: string; |
||||
publicUrl?: string; |
||||
} |
||||
|
||||
export interface Event { |
||||
title: string; |
||||
tn: string; |
||||
url; |
||||
headers; |
||||
operation; |
||||
event; |
||||
retry; |
||||
max; |
||||
interval; |
||||
timeout; |
||||
} |
||||
|
||||
export interface Acl { |
||||
[role: string]: |
||||
| { |
||||
create: boolean | ColumnAcl; |
||||
[key: string]: boolean | ColumnAcl; |
||||
} |
||||
| boolean |
||||
| any; |
||||
} |
||||
|
||||
export interface ColumnAcl { |
||||
columns: { |
||||
[cn: string]: boolean; |
||||
}; |
||||
assign?: { |
||||
[cn: string]: any; |
||||
}; |
||||
} |
||||
|
||||
export interface Acls { |
||||
[tn: string]: Acl; |
||||
} |
||||
|
||||
export enum ServerlessType { |
||||
AWS_LAMBDA = 'AWS_LAMBDA', |
||||
GCP_FUNCTION = 'GCP_FUNCTION', |
||||
AZURE_FUNCTION_APP = 'AZURE_FUNCTION_APP', |
||||
ALIYUN = 'ALIYUN', |
||||
ZEIT = 'ZEIT', |
||||
LYRID = 'LYRID', |
||||
SERVERLESS = 'SERVERLESS', |
||||
} |
||||
|
||||
export class Result { |
||||
public code: any; |
||||
public message: string; |
||||
public data: any; |
||||
|
||||
constructor(code = 0, message = '', data = {}) { |
||||
this.code = code; |
||||
this.message = message; |
||||
this.data = data; |
||||
} |
||||
} |
||||
|
||||
enum HTTPType { |
||||
GET = 'get', |
||||
POST = 'post', |
||||
PUT = 'put', |
||||
DELETE = 'delete', |
||||
PATCH = 'patch', |
||||
HEAD = 'head', |
||||
OPTIONS = 'options', |
||||
} |
||||
|
||||
export interface XcRoute { |
||||
httpType: HTTPType; |
||||
path: string; |
||||
handler: e.Handler; |
||||
dbAlias?: string; |
||||
isCustom?: boolean; |
||||
} |
@ -0,0 +1,142 @@
|
||||
import fs from 'fs'; |
||||
import { promisify } from 'util'; |
||||
import AWS from 'aws-sdk'; |
||||
import request from 'request'; |
||||
import { |
||||
generateTempFilePath, |
||||
waitForStreamClose, |
||||
} from '../../utils/pluginUtils'; |
||||
import type { IStorageAdapterV2, XcFile } from 'nc-plugin'; |
||||
|
||||
export default class Backblaze implements IStorageAdapterV2 { |
||||
private s3Client: AWS.S3; |
||||
private input: any; |
||||
|
||||
constructor(input: any) { |
||||
this.input = input; |
||||
} |
||||
|
||||
async fileCreate(key: string, file: XcFile): Promise<any> { |
||||
const uploadParams: any = { |
||||
ACL: 'public-read', |
||||
ContentType: file.mimetype, |
||||
}; |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
const fileStream = fs.createReadStream(file.path); |
||||
fileStream.on('error', (err) => { |
||||
console.log('File Error', err); |
||||
reject(err); |
||||
}); |
||||
|
||||
uploadParams.Body = fileStream; |
||||
uploadParams.Key = key; |
||||
|
||||
// call S3 to retrieve upload file to specified bucket
|
||||
this.s3Client.upload(uploadParams, (err, data) => { |
||||
if (err) { |
||||
console.log('Error', err); |
||||
reject(err); |
||||
} |
||||
if (data) { |
||||
resolve(data.Location); |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
async fileCreateByUrl(key: string, url: string): Promise<any> { |
||||
const uploadParams: any = { |
||||
ACL: 'public-read', |
||||
}; |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
request( |
||||
{ |
||||
url: url, |
||||
encoding: null, |
||||
}, |
||||
(err, httpResponse, body) => { |
||||
if (err) return reject(err); |
||||
|
||||
uploadParams.Body = body; |
||||
uploadParams.Key = key; |
||||
uploadParams.ContentType = httpResponse.headers['content-type']; |
||||
|
||||
// call S3 to retrieve upload file to specified bucket
|
||||
this.s3Client.upload(uploadParams, (err1, data) => { |
||||
if (err) { |
||||
console.log('Error', err); |
||||
reject(err1); |
||||
} |
||||
if (data) { |
||||
resolve(data.Location); |
||||
} |
||||
}); |
||||
} |
||||
); |
||||
}); |
||||
} |
||||
|
||||
patchRegion(region: string): string { |
||||
// in v0.0.1, we constructed the endpoint with `region = s3.us-west-001`
|
||||
// in v0.0.2, `region` would be `us-west-001`
|
||||
// as backblaze states Region is the 2nd part of your S3 Endpoint in documentation
|
||||
if (region.startsWith('s3.')) { |
||||
region = region.slice(3); |
||||
} |
||||
return region; |
||||
} |
||||
|
||||
public async fileDelete(_path: string): Promise<any> { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
|
||||
public async fileRead(key: string): Promise<any> { |
||||
return new Promise((resolve, reject) => { |
||||
this.s3Client.getObject({ Key: key } as any, (err, data) => { |
||||
if (err) { |
||||
return reject(err); |
||||
} |
||||
if (!data?.Body) { |
||||
return reject(data); |
||||
} |
||||
return resolve(data.Body); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
public async init(): Promise<any> { |
||||
const s3Options: any = { |
||||
params: { Bucket: this.input.bucket }, |
||||
region: this.patchRegion(this.input.region), |
||||
}; |
||||
|
||||
s3Options.accessKeyId = this.input.access_key; |
||||
s3Options.secretAccessKey = this.input.access_secret; |
||||
|
||||
s3Options.endpoint = new AWS.Endpoint( |
||||
`s3.${s3Options.region}.backblazeb2.com` |
||||
); |
||||
|
||||
this.s3Client = new AWS.S3(s3Options); |
||||
} |
||||
|
||||
public async test(): Promise<boolean> { |
||||
try { |
||||
const tempFile = generateTempFilePath(); |
||||
const createStream = fs.createWriteStream(tempFile); |
||||
await waitForStreamClose(createStream); |
||||
await this.fileCreate('nc-test-file.txt', { |
||||
path: tempFile, |
||||
mimetype: 'text/plain', |
||||
originalname: 'temp.txt', |
||||
size: '', |
||||
}); |
||||
await promisify(fs.unlink)(tempFile); |
||||
return true; |
||||
} catch (e) { |
||||
throw e; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcStoragePlugin } from 'nc-plugin'; |
||||
import Backblaze from './Backblaze'; |
||||
import type { IStorageAdapterV2 } from 'nc-plugin'; |
||||
|
||||
class BackblazePlugin extends XcStoragePlugin { |
||||
private static storageAdapter: Backblaze; |
||||
|
||||
public getAdapter(): IStorageAdapterV2 { |
||||
return BackblazePlugin.storageAdapter; |
||||
} |
||||
|
||||
public async init(config: any): Promise<any> { |
||||
BackblazePlugin.storageAdapter = new Backblaze(config); |
||||
await BackblazePlugin.storageAdapter.init(); |
||||
} |
||||
} |
||||
|
||||
export default BackblazePlugin; |
@ -0,0 +1,68 @@
|
||||
import { XcActionType, XcType } from 'nocodb-sdk'; |
||||
import BackblazePlugin from './BackblazePlugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: BackblazePlugin, |
||||
title: 'Backblaze B2', |
||||
version: '0.0.2', |
||||
logo: 'plugins/backblaze.jpeg', |
||||
tags: 'Storage', |
||||
description: |
||||
'Backblaze B2 is enterprise-grade, S3 compatible storage that companies around the world use to store and serve data while improving their cloud OpEx vs. Amazon S3 and others.', |
||||
inputs: { |
||||
title: 'Configure Backblaze B2', |
||||
items: [ |
||||
{ |
||||
key: 'bucket', |
||||
label: 'Bucket Name', |
||||
placeholder: 'Bucket Name', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'region', |
||||
label: 'Region', |
||||
placeholder: 'e.g. us-west-001', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_key', |
||||
label: 'Access Key', |
||||
placeholder: 'i.e. keyID in App Keys', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_secret', |
||||
label: 'Access Secret', |
||||
placeholder: 'i.e. applicationKey in App Keys', |
||||
type: XcType.Password, |
||||
required: true, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
placeholder: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
placeholder: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: |
||||
'Successfully installed and attachment will be stored in Backblaze B2', |
||||
msgOnUninstall: '', |
||||
}, |
||||
category: 'Storage', |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,21 @@
|
||||
import axios from 'axios'; |
||||
import type { IWebhookNotificationAdapter } from 'nc-plugin'; |
||||
|
||||
export default class Discord implements IWebhookNotificationAdapter { |
||||
public init(): Promise<any> { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
|
||||
public async sendMessage(content: string, payload: any): Promise<any> { |
||||
for (const { webhook_url } of payload?.channels) { |
||||
try { |
||||
return await axios.post(webhook_url, { |
||||
content, |
||||
}); |
||||
} catch (e) { |
||||
console.log(e); |
||||
throw e; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcWebhookNotificationPlugin } from 'nc-plugin'; |
||||
import Discord from './Discord'; |
||||
import type { IWebhookNotificationAdapter } from 'nc-plugin'; |
||||
|
||||
class DiscordPlugin extends XcWebhookNotificationPlugin { |
||||
private static notificationAdapter: Discord; |
||||
|
||||
public getAdapter(): IWebhookNotificationAdapter { |
||||
return DiscordPlugin.notificationAdapter; |
||||
} |
||||
|
||||
public async init(_config: any): Promise<any> { |
||||
DiscordPlugin.notificationAdapter = new Discord(); |
||||
await DiscordPlugin.notificationAdapter.init(); |
||||
} |
||||
} |
||||
|
||||
export default DiscordPlugin; |
@ -0,0 +1,56 @@
|
||||
import { XcActionType, XcType } from 'nocodb-sdk'; |
||||
import DiscordPlugin from './DiscordPlugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: DiscordPlugin, |
||||
title: 'Discord', |
||||
version: '0.0.1', |
||||
logo: 'plugins/discord.png', |
||||
description: |
||||
'Discord is the easiest way to talk over voice, video, and text. Talk, chat, hang out, and stay close with your friends and communities.', |
||||
price: 'Free', |
||||
tags: 'Chat', |
||||
category: 'Chat', |
||||
inputs: { |
||||
title: 'Configure Discord', |
||||
array: true, |
||||
items: [ |
||||
{ |
||||
key: 'channel', |
||||
label: 'Channel Name', |
||||
placeholder: 'Channel Name', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'webhook_url', |
||||
label: 'Webhook URL', |
||||
type: XcType.Password, |
||||
placeholder: 'Webhook URL', |
||||
required: true, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
placeholder: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
placeholder: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: |
||||
'Successfully installed and Discord is enabled for notification.', |
||||
msgOnUninstall: '', |
||||
}, |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,129 @@
|
||||
import fs from 'fs'; |
||||
import { promisify } from 'util'; |
||||
import { Storage } from '@google-cloud/storage'; |
||||
import request from 'request'; |
||||
import { |
||||
generateTempFilePath, |
||||
waitForStreamClose, |
||||
} from '../../utils/pluginUtils'; |
||||
import type { IStorageAdapterV2, XcFile } from 'nc-plugin'; |
||||
import type { StorageOptions } from '@google-cloud/storage'; |
||||
|
||||
export default class Gcs implements IStorageAdapterV2 { |
||||
private storageClient: Storage; |
||||
private bucketName: string; |
||||
private input: any; |
||||
|
||||
constructor(input: any) { |
||||
this.input = input; |
||||
} |
||||
|
||||
async fileCreate(key: string, file: XcFile): Promise<any> { |
||||
const uploadResponse = await this.storageClient |
||||
.bucket(this.bucketName) |
||||
.upload(file.path, { |
||||
destination: key, |
||||
// Support for HTTP requests made with `Accept-Encoding: gzip`
|
||||
gzip: true, |
||||
// By setting the option `destination`, you can change the name of the
|
||||
// object you are uploading to a bucket.
|
||||
metadata: { |
||||
// Enable long-lived HTTP caching headers
|
||||
// Use only if the contents of the file will never change
|
||||
// (If the contents will change, use cacheControl: 'no-cache')
|
||||
cacheControl: 'public, max-age=31536000', |
||||
}, |
||||
}); |
||||
|
||||
return uploadResponse[0].publicUrl(); |
||||
} |
||||
|
||||
fileDelete(_path: string): Promise<any> { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
|
||||
public fileRead(key: string): Promise<any> { |
||||
return new Promise((resolve, reject) => { |
||||
const file = this.storageClient.bucket(this.bucketName).file(key); |
||||
// Check for existence, since gcloud-node seemed to be caching the result
|
||||
file.exists((err, exists) => { |
||||
if (exists) { |
||||
file.download((downerr, data) => { |
||||
if (err) { |
||||
return reject(downerr); |
||||
} |
||||
return resolve(data); |
||||
}); |
||||
} else { |
||||
reject(err); |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
public async init(): Promise<any> { |
||||
const options: StorageOptions = {}; |
||||
|
||||
// options.credentials = {
|
||||
// client_email: process.env.NC_GCS_CLIENT_EMAIL,
|
||||
// private_key: process.env.NC_GCS_PRIVATE_KEY
|
||||
// }
|
||||
//
|
||||
// this.bucketName = process.env.NC_GCS_BUCKET;
|
||||
options.credentials = { |
||||
client_email: this.input.client_email, |
||||
// replace \n with real line breaks to avoid
|
||||
// error:0909006C:PEM routines:get_name:no start line
|
||||
private_key: this.input.private_key.replace(/\\n/gm, '\n'), |
||||
}; |
||||
|
||||
// default project ID would be used if it is not provided
|
||||
if (this.input.project_id) { |
||||
options.projectId = this.input.project_id; |
||||
} |
||||
|
||||
this.bucketName = this.input.bucket; |
||||
|
||||
this.storageClient = new Storage(options); |
||||
} |
||||
|
||||
public async test(): Promise<boolean> { |
||||
try { |
||||
const tempFile = generateTempFilePath(); |
||||
const createStream = fs.createWriteStream(tempFile); |
||||
await waitForStreamClose(createStream); |
||||
await this.fileCreate('nc-test-file.txt', { |
||||
path: tempFile, |
||||
mimetype: '', |
||||
originalname: 'temp.txt', |
||||
size: '', |
||||
}); |
||||
await promisify(fs.unlink)(tempFile); |
||||
return true; |
||||
} catch (e) { |
||||
throw e; |
||||
} |
||||
} |
||||
|
||||
fileCreateByUrl(destPath: string, url: string): Promise<any> { |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
request( |
||||
{ |
||||
url: url, |
||||
encoding: null, |
||||
}, |
||||
(err, _, body) => { |
||||
if (err) return reject(err); |
||||
|
||||
this.storageClient |
||||
.bucket(this.bucketName) |
||||
.file(destPath) |
||||
.save(body) |
||||
.then((res) => resolve(res)) |
||||
.catch(reject); |
||||
} |
||||
); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcStoragePlugin } from 'nc-plugin'; |
||||
import Gcs from './Gcs'; |
||||
import type { IStorageAdapterV2 } from 'nc-plugin'; |
||||
|
||||
class GcsPlugin extends XcStoragePlugin { |
||||
private static storageAdapter: Gcs; |
||||
|
||||
public getAdapter(): IStorageAdapterV2 { |
||||
return GcsPlugin.storageAdapter; |
||||
} |
||||
|
||||
public async init(config: any): Promise<any> { |
||||
GcsPlugin.storageAdapter = new Gcs(config); |
||||
await GcsPlugin.storageAdapter.init(); |
||||
} |
||||
} |
||||
|
||||
export default GcsPlugin; |
@ -0,0 +1,69 @@
|
||||
import { XcActionType, XcType } from 'nocodb-sdk'; |
||||
import GcsPlugin from './GcsPlugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: GcsPlugin, |
||||
title: 'GCS', |
||||
version: '0.0.2', |
||||
logo: 'plugins/gcs.png', |
||||
description: |
||||
'Google Cloud Storage is a RESTful online file storage web service for storing and accessing data on Google Cloud Platform infrastructure.', |
||||
price: 'Free', |
||||
tags: 'Storage', |
||||
category: 'Storage', |
||||
inputs: { |
||||
title: 'Configure Google Cloud Storage', |
||||
items: [ |
||||
{ |
||||
key: 'bucket', |
||||
label: 'Bucket Name', |
||||
placeholder: 'Bucket Name', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'client_email', |
||||
label: 'Client Email', |
||||
placeholder: 'Client Email', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'private_key', |
||||
label: 'Private Key', |
||||
placeholder: 'Private Key', |
||||
type: XcType.Password, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'project_id', |
||||
label: 'Project ID', |
||||
placeholder: 'Project ID', |
||||
type: XcType.SingleLineText, |
||||
required: false, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
placeholder: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
placeholder: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: |
||||
'Successfully installed and attachment will be stored in Google Cloud Storage', |
||||
msgOnUninstall: '', |
||||
}, |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,132 @@
|
||||
import fs from 'fs'; |
||||
import { promisify } from 'util'; |
||||
import AWS from 'aws-sdk'; |
||||
import request from 'request'; |
||||
import { |
||||
generateTempFilePath, |
||||
waitForStreamClose, |
||||
} from '../../utils/pluginUtils'; |
||||
import type { IStorageAdapterV2, XcFile } from 'nc-plugin'; |
||||
|
||||
export default class LinodeObjectStorage implements IStorageAdapterV2 { |
||||
private s3Client: AWS.S3; |
||||
private input: any; |
||||
|
||||
constructor(input: any) { |
||||
this.input = input; |
||||
} |
||||
|
||||
async fileCreate(key: string, file: XcFile): Promise<any> { |
||||
const uploadParams: any = { |
||||
ACL: 'public-read', |
||||
ContentType: file.mimetype, |
||||
}; |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
const fileStream = fs.createReadStream(file.path); |
||||
fileStream.on('error', (err) => { |
||||
console.log('File Error', err); |
||||
reject(err); |
||||
}); |
||||
|
||||
uploadParams.Body = fileStream; |
||||
uploadParams.Key = key; |
||||
|
||||
// call S3 to retrieve upload file to specified bucket
|
||||
this.s3Client.upload(uploadParams, (err, data) => { |
||||
if (err) { |
||||
console.log('Error', err); |
||||
reject(err); |
||||
} |
||||
if (data) { |
||||
resolve(data.Location); |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
async fileCreateByUrl(key: string, url: string): Promise<any> { |
||||
const uploadParams: any = { |
||||
ACL: 'public-read', |
||||
}; |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
request( |
||||
{ |
||||
url: url, |
||||
encoding: null, |
||||
}, |
||||
(err, httpResponse, body) => { |
||||
if (err) return reject(err); |
||||
|
||||
uploadParams.Body = body; |
||||
uploadParams.Key = key; |
||||
uploadParams.ContentType = httpResponse.headers['content-type']; |
||||
|
||||
// call S3 to retrieve upload file to specified bucket
|
||||
this.s3Client.upload(uploadParams, (err1, data) => { |
||||
if (err) { |
||||
console.log('Error', err); |
||||
reject(err1); |
||||
} |
||||
if (data) { |
||||
resolve(data.Location); |
||||
} |
||||
}); |
||||
} |
||||
); |
||||
}); |
||||
} |
||||
|
||||
public async fileDelete(_path: string): Promise<any> { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
|
||||
public async fileRead(key: string): Promise<any> { |
||||
return new Promise((resolve, reject) => { |
||||
this.s3Client.getObject({ Key: key } as any, (err, data) => { |
||||
if (err) { |
||||
return reject(err); |
||||
} |
||||
if (!data?.Body) { |
||||
return reject(data); |
||||
} |
||||
return resolve(data.Body); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
public async init(): Promise<any> { |
||||
const s3Options: any = { |
||||
params: { Bucket: this.input.bucket }, |
||||
region: this.input.region, |
||||
}; |
||||
|
||||
s3Options.accessKeyId = this.input.access_key; |
||||
s3Options.secretAccessKey = this.input.access_secret; |
||||
|
||||
s3Options.endpoint = new AWS.Endpoint( |
||||
`${this.input.region}.linodeobjects.com` |
||||
); |
||||
|
||||
this.s3Client = new AWS.S3(s3Options); |
||||
} |
||||
|
||||
public async test(): Promise<boolean> { |
||||
try { |
||||
const tempFile = generateTempFilePath(); |
||||
const createStream = fs.createWriteStream(tempFile); |
||||
await waitForStreamClose(createStream); |
||||
await this.fileCreate('nc-test-file.txt', { |
||||
path: tempFile, |
||||
mimetype: 'text/plain', |
||||
originalname: 'temp.txt', |
||||
size: '', |
||||
}); |
||||
await promisify(fs.unlink)(tempFile); |
||||
return true; |
||||
} catch (e) { |
||||
throw e; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcStoragePlugin } from 'nc-plugin'; |
||||
import LinodeObjectStorage from './LinodeObjectStorage'; |
||||
import type { IStorageAdapterV2 } from 'nc-plugin'; |
||||
|
||||
class LinodeObjectStoragePlugin extends XcStoragePlugin { |
||||
private static storageAdapter: LinodeObjectStorage; |
||||
|
||||
public getAdapter(): IStorageAdapterV2 { |
||||
return LinodeObjectStoragePlugin.storageAdapter; |
||||
} |
||||
|
||||
public async init(config: any): Promise<any> { |
||||
LinodeObjectStoragePlugin.storageAdapter = new LinodeObjectStorage(config); |
||||
await LinodeObjectStoragePlugin.storageAdapter.init(); |
||||
} |
||||
} |
||||
|
||||
export default LinodeObjectStoragePlugin; |
@ -0,0 +1,68 @@
|
||||
import { XcActionType, XcType } from 'nocodb-sdk'; |
||||
import LinodeObjectStoragePlugin from './LinodeObjectStoragePlugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: LinodeObjectStoragePlugin, |
||||
title: 'Linode Object Storage', |
||||
version: '0.0.1', |
||||
logo: 'plugins/linode.svg', |
||||
tags: 'Storage', |
||||
description: |
||||
'S3-compatible Linode Object Storage makes it easy and more affordable to manage unstructured data such as content assets, as well as sophisticated and data-intensive storage challenges around artificial intelligence and machine learning.', |
||||
inputs: { |
||||
title: 'Configure Linode Object Storage', |
||||
items: [ |
||||
{ |
||||
key: 'bucket', |
||||
label: 'Bucket Name', |
||||
placeholder: 'Bucket Name', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'region', |
||||
label: 'Region', |
||||
placeholder: 'Region', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_key', |
||||
label: 'Access Key', |
||||
placeholder: 'Access Key', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_secret', |
||||
label: 'Access Secret', |
||||
placeholder: 'Access Secret', |
||||
type: XcType.Password, |
||||
required: true, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
placeholder: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
placeholder: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: |
||||
'Successfully installed and attachment will be stored in Linode Object Storage', |
||||
msgOnUninstall: '', |
||||
}, |
||||
category: 'Storage', |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,48 @@
|
||||
import MailerSend, { EmailParams, Recipient } from 'mailersend'; |
||||
import type { IEmailAdapter } from 'nc-plugin'; |
||||
import type { XcEmail } from '../../interface/IEmailAdapter'; |
||||
|
||||
export default class Mailer implements IEmailAdapter { |
||||
private mailersend: MailerSend; |
||||
private input: any; |
||||
|
||||
constructor(input: any) { |
||||
this.input = input; |
||||
} |
||||
|
||||
public async init(): Promise<any> { |
||||
this.mailersend = new MailerSend({ |
||||
api_key: this.input?.api_key, |
||||
}); |
||||
} |
||||
|
||||
public async mailSend(mail: XcEmail): Promise<any> { |
||||
const recipients = [new Recipient(mail.to)]; |
||||
|
||||
const emailParams = new EmailParams() |
||||
.setFrom(this.input.from) |
||||
.setFromName(this.input.from_name) |
||||
.setRecipients(recipients) |
||||
.setSubject(mail.subject) |
||||
.setHtml(mail.html) |
||||
.setText(mail.text); |
||||
|
||||
const res = await this.mailersend.send(emailParams); |
||||
if (res.status === 401) { |
||||
throw new Error(res.status); |
||||
} |
||||
} |
||||
|
||||
public async test(email): Promise<boolean> { |
||||
try { |
||||
await this.mailSend({ |
||||
to: email, |
||||
subject: 'Test email', |
||||
html: 'Test email', |
||||
} as any); |
||||
return true; |
||||
} catch (e) { |
||||
throw e; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcEmailPlugin } from 'nc-plugin'; |
||||
import MailerSend from './MailerSend'; |
||||
import type { IEmailAdapter } from 'nc-plugin'; |
||||
|
||||
class MailerSendPlugin extends XcEmailPlugin { |
||||
private static storageAdapter: MailerSend; |
||||
|
||||
public getAdapter(): IEmailAdapter { |
||||
return MailerSendPlugin.storageAdapter; |
||||
} |
||||
|
||||
public async init(config: any): Promise<any> { |
||||
MailerSendPlugin.storageAdapter = new MailerSend(config); |
||||
await MailerSendPlugin.storageAdapter.init(); |
||||
} |
||||
} |
||||
|
||||
export default MailerSendPlugin; |
@ -0,0 +1,60 @@
|
||||
import { XcActionType, XcType } from 'nocodb-sdk'; |
||||
import MailerSendPlugin from './MailerSendPlugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: MailerSendPlugin, |
||||
title: 'MailerSend', |
||||
version: '0.0.1', |
||||
logo: 'plugins/mailersend.svg', |
||||
// icon: 'mdi-email-outline',
|
||||
description: 'MailerSend email client', |
||||
price: 'Free', |
||||
tags: 'Email', |
||||
category: 'Email', |
||||
inputs: { |
||||
title: 'Configure MailerSend', |
||||
items: [ |
||||
{ |
||||
key: 'api_key', |
||||
label: 'API KEy', |
||||
placeholder: 'eg: ***************', |
||||
type: XcType.Password, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'from', |
||||
label: 'From', |
||||
placeholder: 'eg: admin@run.com', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'from_name', |
||||
label: 'From Name', |
||||
placeholder: 'eg: Adam', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: |
||||
'Successfully installed and email notification will use MailerSend configuration', |
||||
msgOnUninstall: '', |
||||
}, |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,21 @@
|
||||
import axios from 'axios'; |
||||
import type { IWebhookNotificationAdapter } from 'nc-plugin'; |
||||
|
||||
export default class Mattermost implements IWebhookNotificationAdapter { |
||||
public init(): Promise<any> { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
|
||||
public async sendMessage(text: string, payload: any): Promise<any> { |
||||
for (const { webhook_url } of payload?.channels) { |
||||
try { |
||||
return await axios.post(webhook_url, { |
||||
text, |
||||
}); |
||||
} catch (e) { |
||||
console.log(e); |
||||
throw e; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcWebhookNotificationPlugin } from 'nc-plugin'; |
||||
import Mattermost from './Mattermost'; |
||||
import type { IWebhookNotificationAdapter } from 'nc-plugin'; |
||||
|
||||
class MattermostPlugin extends XcWebhookNotificationPlugin { |
||||
private static notificationAdapter: Mattermost; |
||||
|
||||
public getAdapter(): IWebhookNotificationAdapter { |
||||
return MattermostPlugin.notificationAdapter; |
||||
} |
||||
|
||||
public async init(_config: any): Promise<any> { |
||||
MattermostPlugin.notificationAdapter = new Mattermost(); |
||||
await MattermostPlugin.notificationAdapter.init(); |
||||
} |
||||
} |
||||
|
||||
export default MattermostPlugin; |
@ -0,0 +1,56 @@
|
||||
import { XcActionType, XcType } from 'nocodb-sdk'; |
||||
import MattermostPlugin from './MattermostPlugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: MattermostPlugin, |
||||
title: 'Mattermost', |
||||
version: '0.0.1', |
||||
logo: 'plugins/mattermost.png', |
||||
description: |
||||
'Mattermost brings all your team communication into one place, making it searchable and accessible anywhere.', |
||||
price: 'Free', |
||||
tags: 'Chat', |
||||
category: 'Chat', |
||||
inputs: { |
||||
title: 'Configure Mattermost', |
||||
array: true, |
||||
items: [ |
||||
{ |
||||
key: 'channel', |
||||
label: 'Channel Name', |
||||
placeholder: 'Channel Name', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'webhook_url', |
||||
label: 'Webhook URL', |
||||
placeholder: 'Webhook URL', |
||||
type: XcType.Password, |
||||
required: true, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
placeholder: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
placeholder: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: |
||||
'Successfully installed and Mattermost is enabled for notification.', |
||||
msgOnUninstall: '', |
||||
}, |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,134 @@
|
||||
import fs from 'fs'; |
||||
import { promisify } from 'util'; |
||||
import { Client as MinioClient } from 'minio'; |
||||
import request from 'request'; |
||||
import { |
||||
generateTempFilePath, |
||||
waitForStreamClose, |
||||
} from '../../utils/pluginUtils'; |
||||
import type { IStorageAdapterV2, XcFile } from 'nc-plugin'; |
||||
|
||||
export default class Minio implements IStorageAdapterV2 { |
||||
private minioClient: MinioClient; |
||||
private input: any; |
||||
|
||||
constructor(input: any) { |
||||
this.input = input; |
||||
} |
||||
|
||||
async fileCreate(key: string, file: XcFile): Promise<any> { |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
const fileStream = fs.createReadStream(file.path); |
||||
fileStream.on('error', (err) => { |
||||
console.log('File Error', err); |
||||
reject(err); |
||||
}); |
||||
|
||||
// uploadParams.Body = fileStream;
|
||||
// uploadParams.Key = key;
|
||||
const metaData = { |
||||
'Content-Type': file.mimetype, |
||||
// 'X-Amz-Meta-Testing': 1234,
|
||||
// 'run': 5678
|
||||
}; |
||||
// call S3 to retrieve upload file to specified bucket
|
||||
this.minioClient |
||||
.putObject(this.input?.bucket, key, fileStream, metaData) |
||||
.then(() => { |
||||
resolve( |
||||
`http${this.input.useSSL ? 's' : ''}://${this.input.endPoint}:${ |
||||
this.input.port |
||||
}/${this.input.bucket}/${key}` |
||||
); |
||||
}) |
||||
.catch(reject); |
||||
}); |
||||
} |
||||
|
||||
public async fileDelete(_path: string): Promise<any> { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
|
||||
public async fileRead(key: string): Promise<any> { |
||||
return new Promise((resolve, reject) => { |
||||
this.minioClient.getObject(this.input.bucket, key, (err, data) => { |
||||
if (err) { |
||||
return reject(err); |
||||
} |
||||
if (!data) { |
||||
return reject(data); |
||||
} |
||||
return resolve(data); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
public async init(): Promise<any> { |
||||
// todo: update in ui(checkbox and number field)
|
||||
this.input.port = +this.input.port || 9000; |
||||
this.input.useSSL = this.input.useSSL === true; |
||||
this.input.accessKey = this.input.access_key; |
||||
this.input.secretKey = this.input.access_secret; |
||||
|
||||
this.minioClient = new MinioClient(this.input); |
||||
} |
||||
|
||||
public async test(): Promise<boolean> { |
||||
try { |
||||
const tempFile = generateTempFilePath(); |
||||
const createStream = fs.createWriteStream(tempFile); |
||||
await waitForStreamClose(createStream); |
||||
await this.fileCreate('nc-test-file.txt', { |
||||
path: tempFile, |
||||
mimetype: '', |
||||
originalname: 'temp.txt', |
||||
size: '', |
||||
}); |
||||
await promisify(fs.unlink)(tempFile); |
||||
return true; |
||||
} catch (e) { |
||||
throw e; |
||||
} |
||||
} |
||||
|
||||
async fileCreateByUrl(key: string, url: string): Promise<any> { |
||||
const uploadParams: any = { |
||||
ACL: 'public-read', |
||||
}; |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
request( |
||||
{ |
||||
url: url, |
||||
encoding: null, |
||||
}, |
||||
(err, _, body) => { |
||||
if (err) return reject(err); |
||||
|
||||
uploadParams.Body = body; |
||||
uploadParams.Key = key; |
||||
|
||||
// uploadParams.Body = fileStream;
|
||||
// uploadParams.Key = key;
|
||||
const metaData = { |
||||
// 'Content-Type': file.mimetype
|
||||
// 'X-Amz-Meta-Testing': 1234,
|
||||
// 'run': 5678
|
||||
}; |
||||
// call S3 to retrieve upload file to specified bucket
|
||||
this.minioClient |
||||
.putObject(this.input?.bucket, key, body, metaData) |
||||
.then(() => { |
||||
resolve( |
||||
`http${this.input.useSSL ? 's' : ''}://${this.input.endPoint}:${ |
||||
this.input.port |
||||
}/${this.input.bucket}/${key}` |
||||
); |
||||
}) |
||||
.catch(reject); |
||||
} |
||||
); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcStoragePlugin } from 'nc-plugin'; |
||||
import Minio from './Minio'; |
||||
import type { IStorageAdapterV2 } from 'nc-plugin'; |
||||
|
||||
class MinioPlugin extends XcStoragePlugin { |
||||
private static storageAdapter: Minio; |
||||
|
||||
public getAdapter(): IStorageAdapterV2 { |
||||
return MinioPlugin.storageAdapter; |
||||
} |
||||
|
||||
public async init(config: any): Promise<any> { |
||||
MinioPlugin.storageAdapter = new Minio(config); |
||||
await MinioPlugin.storageAdapter.init(); |
||||
} |
||||
} |
||||
|
||||
export default MinioPlugin; |
@ -0,0 +1,83 @@
|
||||
import { XcActionType, XcType } from 'nocodb-sdk'; |
||||
import S3Plugin from './MinioPlugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: S3Plugin, |
||||
title: 'Minio', |
||||
version: '0.0.1', |
||||
logo: 'plugins/minio.png', |
||||
description: |
||||
'MinIO is a High Performance Object Storage released under Apache License v2.0. It is API compatible with Amazon S3 cloud storage service.', |
||||
price: 'Free', |
||||
tags: 'Storage', |
||||
category: 'Storage', |
||||
inputs: { |
||||
title: 'Configure Minio', |
||||
items: [ |
||||
{ |
||||
key: 'endPoint', |
||||
label: 'Minio Endpoint', |
||||
placeholder: 'Minio Endpoint', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'port', |
||||
label: 'Port', |
||||
placeholder: 'Port', |
||||
type: XcType.Number, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'bucket', |
||||
label: 'Bucket Name', |
||||
placeholder: 'Bucket Name', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_key', |
||||
label: 'Access Key', |
||||
placeholder: 'Access Key', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_secret', |
||||
label: 'Access Secret', |
||||
placeholder: 'Access Secret', |
||||
type: XcType.Password, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'useSSL', |
||||
label: 'Use SSL', |
||||
placeholder: 'Use SSL', |
||||
type: XcType.Checkbox, |
||||
required: true, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
placeholder: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
placeholder: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: |
||||
'Successfully installed and attachment will be stored in Minio', |
||||
msgOnUninstall: '', |
||||
}, |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,132 @@
|
||||
import fs from 'fs'; |
||||
import { promisify } from 'util'; |
||||
import AWS from 'aws-sdk'; |
||||
import request from 'request'; |
||||
import { |
||||
generateTempFilePath, |
||||
waitForStreamClose, |
||||
} from '../../utils/pluginUtils'; |
||||
import type { IStorageAdapterV2, XcFile } from 'nc-plugin'; |
||||
|
||||
export default class OvhCloud implements IStorageAdapterV2 { |
||||
private s3Client: AWS.S3; |
||||
private input: any; |
||||
|
||||
constructor(input: any) { |
||||
this.input = input; |
||||
} |
||||
|
||||
async fileCreate(key: string, file: XcFile): Promise<any> { |
||||
const uploadParams: any = { |
||||
ACL: 'public-read', |
||||
ContentType: file.mimetype, |
||||
}; |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
const fileStream = fs.createReadStream(file.path); |
||||
fileStream.on('error', (err) => { |
||||
console.log('File Error', err); |
||||
reject(err); |
||||
}); |
||||
|
||||
uploadParams.Body = fileStream; |
||||
uploadParams.Key = key; |
||||
|
||||
// call S3 to retrieve upload file to specified bucket
|
||||
this.s3Client.upload(uploadParams, (err, data) => { |
||||
if (err) { |
||||
console.log('Error', err); |
||||
reject(err); |
||||
} |
||||
if (data) { |
||||
resolve(data.Location); |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
async fileCreateByUrl(key: string, url: string): Promise<any> { |
||||
const uploadParams: any = { |
||||
ACL: 'public-read', |
||||
}; |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
request( |
||||
{ |
||||
url: url, |
||||
encoding: null, |
||||
}, |
||||
(err, httpResponse, body) => { |
||||
if (err) return reject(err); |
||||
|
||||
uploadParams.Body = body; |
||||
uploadParams.Key = key; |
||||
uploadParams.ContentType = httpResponse.headers['content-type']; |
||||
|
||||
// call S3 to retrieve upload file to specified bucket
|
||||
this.s3Client.upload(uploadParams, (err1, data) => { |
||||
if (err) { |
||||
console.log('Error', err); |
||||
reject(err1); |
||||
} |
||||
if (data) { |
||||
resolve(data.Location); |
||||
} |
||||
}); |
||||
} |
||||
); |
||||
}); |
||||
} |
||||
|
||||
public async fileDelete(_path: string): Promise<any> { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
|
||||
public async fileRead(key: string): Promise<any> { |
||||
return new Promise((resolve, reject) => { |
||||
this.s3Client.getObject({ Key: key } as any, (err, data) => { |
||||
if (err) { |
||||
return reject(err); |
||||
} |
||||
if (!data?.Body) { |
||||
return reject(data); |
||||
} |
||||
return resolve(data.Body); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
public async init(): Promise<any> { |
||||
const s3Options: any = { |
||||
params: { Bucket: this.input.bucket }, |
||||
region: this.input.region, |
||||
}; |
||||
|
||||
s3Options.accessKeyId = this.input.access_key; |
||||
s3Options.secretAccessKey = this.input.access_secret; |
||||
|
||||
s3Options.endpoint = new AWS.Endpoint( |
||||
`s3.${this.input.region}.cloud.ovh.net` |
||||
); |
||||
|
||||
this.s3Client = new AWS.S3(s3Options); |
||||
} |
||||
|
||||
public async test(): Promise<boolean> { |
||||
try { |
||||
const tempFile = generateTempFilePath(); |
||||
const createStream = fs.createWriteStream(tempFile); |
||||
await waitForStreamClose(createStream); |
||||
await this.fileCreate('nc-test-file.txt', { |
||||
path: tempFile, |
||||
mimetype: 'text/plain', |
||||
originalname: 'temp.txt', |
||||
size: '', |
||||
}); |
||||
await promisify(fs.unlink)(tempFile); |
||||
return true; |
||||
} catch (e) { |
||||
throw e; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcStoragePlugin } from 'nc-plugin'; |
||||
import OvhCloud from './OvhCloud'; |
||||
import type { IStorageAdapterV2 } from 'nc-plugin'; |
||||
|
||||
class OvhCloudPlugin extends XcStoragePlugin { |
||||
private static storageAdapter: OvhCloud; |
||||
|
||||
public getAdapter(): IStorageAdapterV2 { |
||||
return OvhCloudPlugin.storageAdapter; |
||||
} |
||||
|
||||
public async init(config: any): Promise<any> { |
||||
OvhCloudPlugin.storageAdapter = new OvhCloud(config); |
||||
await OvhCloudPlugin.storageAdapter.init(); |
||||
} |
||||
} |
||||
|
||||
export default OvhCloudPlugin; |
@ -0,0 +1,68 @@
|
||||
import { XcActionType, XcType } from 'nocodb-sdk'; |
||||
import OvhCloud from './OvhCloudPlugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: OvhCloud, |
||||
title: 'OvhCloud Object Storage', |
||||
version: '0.0.1', |
||||
logo: 'plugins/ovhCloud.png', |
||||
tags: 'Storage', |
||||
description: |
||||
'Upload your files to a space that you can access via HTTPS using the OpenStack Swift API, or the S3 API. ', |
||||
inputs: { |
||||
title: 'Configure OvhCloud Object Storage', |
||||
items: [ |
||||
{ |
||||
key: 'bucket', |
||||
label: 'Bucket Name', |
||||
placeholder: 'Bucket Name', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'region', |
||||
label: 'Region', |
||||
placeholder: 'Region', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_key', |
||||
label: 'Access Key', |
||||
placeholder: 'Access Key', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_secret', |
||||
label: 'Access Secret', |
||||
placeholder: 'Access Secret', |
||||
type: XcType.Password, |
||||
required: true, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
placeholder: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
placeholder: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: |
||||
'Successfully installed and attachment will be stored in OvhCloud Object Storage', |
||||
msgOnUninstall: '', |
||||
}, |
||||
category: 'Storage', |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,135 @@
|
||||
import fs from 'fs'; |
||||
import { promisify } from 'util'; |
||||
import AWS from 'aws-sdk'; |
||||
import request from 'request'; |
||||
import { |
||||
generateTempFilePath, |
||||
waitForStreamClose, |
||||
} from '../../utils/pluginUtils'; |
||||
import type { IStorageAdapterV2, XcFile } from 'nc-plugin'; |
||||
|
||||
export default class S3 implements IStorageAdapterV2 { |
||||
private s3Client: AWS.S3; |
||||
private input: any; |
||||
|
||||
constructor(input: any) { |
||||
this.input = input; |
||||
} |
||||
|
||||
async fileCreate(key: string, file: XcFile): Promise<any> { |
||||
const uploadParams: any = { |
||||
ACL: 'public-read', |
||||
ContentType: file.mimetype, |
||||
}; |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
const fileStream = fs.createReadStream(file.path); |
||||
fileStream.on('error', (err) => { |
||||
console.log('File Error', err); |
||||
reject(err); |
||||
}); |
||||
|
||||
uploadParams.Body = fileStream; |
||||
uploadParams.Key = key; |
||||
|
||||
// call S3 to retrieve upload file to specified bucket
|
||||
this.s3Client.upload(uploadParams, (err, data) => { |
||||
if (err) { |
||||
console.log('Error', err); |
||||
reject(err); |
||||
} |
||||
if (data) { |
||||
resolve(data.Location); |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
async fileCreateByUrl(key: string, url: string): Promise<any> { |
||||
const uploadParams: any = { |
||||
ACL: 'public-read', |
||||
}; |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
request( |
||||
{ |
||||
url: url, |
||||
encoding: null, |
||||
}, |
||||
(err, httpResponse, body) => { |
||||
if (err) return reject(err); |
||||
|
||||
uploadParams.Body = body; |
||||
uploadParams.Key = key; |
||||
uploadParams.ContentType = httpResponse.headers['content-type']; |
||||
|
||||
// call S3 to retrieve upload file to specified bucket
|
||||
this.s3Client.upload(uploadParams, (err1, data) => { |
||||
if (err) { |
||||
console.log('Error', err); |
||||
reject(err1); |
||||
} |
||||
if (data) { |
||||
resolve(data.Location); |
||||
} |
||||
}); |
||||
} |
||||
); |
||||
}); |
||||
} |
||||
|
||||
public async fileDelete(_path: string): Promise<any> { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
|
||||
public async fileRead(key: string): Promise<any> { |
||||
return new Promise((resolve, reject) => { |
||||
this.s3Client.getObject({ Key: key } as any, (err, data) => { |
||||
if (err) { |
||||
return reject(err); |
||||
} |
||||
if (!data?.Body) { |
||||
return reject(data); |
||||
} |
||||
return resolve(data.Body); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
public async init(): Promise<any> { |
||||
// const s3Options: any = {
|
||||
// params: {Bucket: process.env.NC_S3_BUCKET},
|
||||
// region: process.env.NC_S3_REGION
|
||||
// };
|
||||
//
|
||||
// s3Options.accessKeyId = process.env.NC_S3_KEY;
|
||||
// s3Options.secretAccessKey = process.env.NC_S3_SECRET;
|
||||
|
||||
const s3Options: any = { |
||||
params: { Bucket: this.input.bucket }, |
||||
region: this.input.region, |
||||
}; |
||||
|
||||
s3Options.accessKeyId = this.input.access_key; |
||||
s3Options.secretAccessKey = this.input.access_secret; |
||||
|
||||
this.s3Client = new AWS.S3(s3Options); |
||||
} |
||||
|
||||
public async test(): Promise<boolean> { |
||||
try { |
||||
const tempFile = generateTempFilePath(); |
||||
const createStream = fs.createWriteStream(tempFile); |
||||
await waitForStreamClose(createStream); |
||||
await this.fileCreate('nc-test-file.txt', { |
||||
path: tempFile, |
||||
mimetype: 'text/plain', |
||||
originalname: 'temp.txt', |
||||
size: '', |
||||
}); |
||||
await promisify(fs.unlink)(tempFile); |
||||
return true; |
||||
} catch (e) { |
||||
throw e; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcStoragePlugin } from 'nc-plugin'; |
||||
import S3 from './S3'; |
||||
import type { IStorageAdapterV2 } from 'nc-plugin'; |
||||
|
||||
class S3Plugin extends XcStoragePlugin { |
||||
private static storageAdapter: S3; |
||||
|
||||
public getAdapter(): IStorageAdapterV2 { |
||||
return S3Plugin.storageAdapter; |
||||
} |
||||
|
||||
public async init(config: any): Promise<any> { |
||||
S3Plugin.storageAdapter = new S3(config); |
||||
await S3Plugin.storageAdapter.init(); |
||||
} |
||||
} |
||||
|
||||
export default S3Plugin; |
@ -0,0 +1,68 @@
|
||||
import { PluginCategory, XcActionType, XcType } from 'nocodb-sdk'; |
||||
import S3Plugin from './S3Plugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: S3Plugin, |
||||
title: 'S3', |
||||
version: '0.0.1', |
||||
logo: 'plugins/s3.png', |
||||
description: |
||||
'Amazon Simple Storage Service (Amazon S3) is an object storage service that offers industry-leading scalability, data availability, security, and performance.', |
||||
inputs: { |
||||
title: 'Configure Amazon S3', |
||||
items: [ |
||||
{ |
||||
key: 'bucket', |
||||
label: 'Bucket Name', |
||||
placeholder: 'Bucket Name', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'region', |
||||
label: 'Region', |
||||
placeholder: 'Region', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_key', |
||||
label: 'Access Key', |
||||
placeholder: 'Access Key', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_secret', |
||||
label: 'Access Secret', |
||||
placeholder: 'Access Secret', |
||||
type: XcType.Password, |
||||
required: true, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
placeholder: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
placeholder: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: |
||||
'Successfully installed and attachment will be stored in AWS S3', |
||||
msgOnUninstall: '', |
||||
}, |
||||
category: PluginCategory.STORAGE, |
||||
tags: 'Storage', |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,130 @@
|
||||
import fs from 'fs'; |
||||
import { promisify } from 'util'; |
||||
import AWS from 'aws-sdk'; |
||||
import request from 'request'; |
||||
import { |
||||
generateTempFilePath, |
||||
waitForStreamClose, |
||||
} from '../../utils/pluginUtils'; |
||||
import type { IStorageAdapterV2, XcFile } from 'nc-plugin'; |
||||
|
||||
export default class ScalewayObjectStorage implements IStorageAdapterV2 { |
||||
private s3Client: AWS.S3; |
||||
private input: any; |
||||
|
||||
constructor(input: any) { |
||||
this.input = input; |
||||
} |
||||
|
||||
public async fileRead(key: string): Promise<any> { |
||||
return new Promise((resolve, reject) => { |
||||
this.s3Client.getObject({ Key: key } as any, (err, data) => { |
||||
if (err) { |
||||
return reject(err); |
||||
} |
||||
if (!data?.Body) { |
||||
return reject(data); |
||||
} |
||||
return resolve(data.Body); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
public async test(): Promise<boolean> { |
||||
try { |
||||
const tempFile = generateTempFilePath(); |
||||
const createStream = fs.createWriteStream(tempFile); |
||||
await waitForStreamClose(createStream); |
||||
await this.fileCreate('nc-test-file.txt', { |
||||
path: tempFile, |
||||
mimetype: 'text/plain', |
||||
originalname: 'temp.txt', |
||||
size: '', |
||||
}); |
||||
await promisify(fs.unlink)(tempFile); |
||||
return true; |
||||
} catch (e) { |
||||
throw e; |
||||
} |
||||
} |
||||
|
||||
public async fileDelete(_path: string): Promise<any> { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
|
||||
public async init(): Promise<any> { |
||||
const s3Options: any = { |
||||
params: { Bucket: this.input.bucket }, |
||||
region: this.input.region, |
||||
}; |
||||
|
||||
s3Options.accessKeyId = this.input.access_key; |
||||
s3Options.secretAccessKey = this.input.access_secret; |
||||
|
||||
s3Options.endpoint = new AWS.Endpoint(`s3.${this.input.region}.scw.cloud`); |
||||
|
||||
this.s3Client = new AWS.S3(s3Options); |
||||
} |
||||
|
||||
async fileCreate(key: string, file: XcFile): Promise<any> { |
||||
const uploadParams: any = { |
||||
ACL: 'public-read', |
||||
ContentType: file.mimetype, |
||||
}; |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
const fileStream = fs.createReadStream(file.path); |
||||
fileStream.on('error', (err) => { |
||||
console.log('File Error', err); |
||||
reject(err); |
||||
}); |
||||
|
||||
uploadParams.Body = fileStream; |
||||
uploadParams.Key = key; |
||||
|
||||
// call S3 to retrieve upload file to specified bucket
|
||||
this.s3Client.upload(uploadParams, (err, data) => { |
||||
if (err) { |
||||
console.log('Error', err); |
||||
reject(err); |
||||
} |
||||
if (data) { |
||||
resolve(data.Location); |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
async fileCreateByUrl(key: string, url: string): Promise<any> { |
||||
const uploadParams: any = { |
||||
ACL: 'public-read', |
||||
}; |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
request( |
||||
{ |
||||
url: url, |
||||
encoding: null, |
||||
}, |
||||
(err, httpResponse, body) => { |
||||
if (err) return reject(err); |
||||
|
||||
uploadParams.Body = body; |
||||
uploadParams.Key = key; |
||||
uploadParams.ContentType = httpResponse.headers['content-type']; |
||||
|
||||
// call S3 to retrieve upload file to specified bucket
|
||||
this.s3Client.upload(uploadParams, (err1, data) => { |
||||
if (err) { |
||||
console.log('Error', err); |
||||
reject(err1); |
||||
} |
||||
if (data) { |
||||
resolve(data.Location); |
||||
} |
||||
}); |
||||
} |
||||
); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcStoragePlugin } from 'nc-plugin'; |
||||
import ScalewayObjectStorage from './ScalewayObjectStorage'; |
||||
import type { IStorageAdapterV2 } from 'nc-plugin'; |
||||
|
||||
class ScalewayObjectStoragePlugin extends XcStoragePlugin { |
||||
private static storageAdapter: ScalewayObjectStorage; |
||||
public async init(config: any): Promise<any> { |
||||
ScalewayObjectStoragePlugin.storageAdapter = new ScalewayObjectStorage( |
||||
config |
||||
); |
||||
await ScalewayObjectStoragePlugin.storageAdapter.init(); |
||||
} |
||||
public getAdapter(): IStorageAdapterV2 { |
||||
return ScalewayObjectStoragePlugin.storageAdapter; |
||||
} |
||||
} |
||||
|
||||
export default ScalewayObjectStoragePlugin; |
@ -0,0 +1,67 @@
|
||||
import { XcActionType, XcType } from 'nocodb-sdk'; |
||||
import ScalewayObjectStoragePlugin from './ScalewayObjectStoragePlugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: ScalewayObjectStoragePlugin, |
||||
title: 'Scaleway Object Storage', |
||||
version: '0.0.1', |
||||
logo: 'plugins/scaleway.png', |
||||
tags: 'Storage', |
||||
description: |
||||
'Scaleway Object Storage is an S3-compatible object store from Scaleway Cloud Platform.', |
||||
inputs: { |
||||
title: 'Setup Scaleway', |
||||
items: [ |
||||
{ |
||||
key: 'bucket', |
||||
label: 'Bucket name', |
||||
placeholder: 'Bucket name', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'region', |
||||
label: 'Region of bucket', |
||||
placeholder: 'Region of bucket', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_key', |
||||
label: 'Access Key', |
||||
placeholder: 'Access Key', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_secret', |
||||
label: 'Access Secret', |
||||
placeholder: 'Access Secret', |
||||
type: XcType.Password, |
||||
required: true, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
placeholder: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
placeholder: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: 'Successfully installed Scaleway Object Storage', |
||||
msgOnUninstall: '', |
||||
}, |
||||
category: 'Storage', |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,54 @@
|
||||
import nodemailer from 'nodemailer'; |
||||
import AWS from 'aws-sdk'; |
||||
import type { IEmailAdapter } from 'nc-plugin'; |
||||
import type Mail from 'nodemailer/lib/mailer'; |
||||
import type { XcEmail } from '../../interface/IEmailAdapter'; |
||||
|
||||
export default class SES implements IEmailAdapter { |
||||
private transporter: Mail; |
||||
private input: any; |
||||
|
||||
constructor(input: any) { |
||||
this.input = input; |
||||
} |
||||
|
||||
public async init(): Promise<any> { |
||||
const sesOptions: any = { |
||||
accessKeyId: this.input.access_key, |
||||
secretAccessKey: this.input.access_secret, |
||||
region: this.input.region, |
||||
}; |
||||
|
||||
this.transporter = nodemailer.createTransport({ |
||||
SES: new AWS.SES(sesOptions), |
||||
}); |
||||
} |
||||
|
||||
public async mailSend(mail: XcEmail): Promise<any> { |
||||
if (this.transporter) { |
||||
this.transporter.sendMail( |
||||
{ ...mail, from: this.input.from }, |
||||
(err, info) => { |
||||
if (err) { |
||||
console.log(err); |
||||
} else { |
||||
console.log('Message sent: ' + info.response); |
||||
} |
||||
} |
||||
); |
||||
} |
||||
} |
||||
|
||||
public async test(): Promise<boolean> { |
||||
try { |
||||
await this.mailSend({ |
||||
to: this.input.from, |
||||
subject: 'Test email', |
||||
html: 'Test email', |
||||
} as any); |
||||
return true; |
||||
} catch (e) { |
||||
throw e; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcEmailPlugin } from 'nc-plugin'; |
||||
import SES from './SES'; |
||||
import type { IEmailAdapter } from 'nc-plugin'; |
||||
|
||||
class SESPlugin extends XcEmailPlugin { |
||||
private static storageAdapter: SES; |
||||
|
||||
public getAdapter(): IEmailAdapter { |
||||
return SESPlugin.storageAdapter; |
||||
} |
||||
|
||||
public async init(config: any): Promise<any> { |
||||
SESPlugin.storageAdapter = new SES(config); |
||||
await SESPlugin.storageAdapter.init(); |
||||
} |
||||
} |
||||
|
||||
export default SESPlugin; |
@ -0,0 +1,69 @@
|
||||
import { XcActionType, XcType } from 'nocodb-sdk'; |
||||
import SESPlugin from './SESPlugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: SESPlugin, |
||||
title: 'SES', |
||||
version: '0.0.1', |
||||
logo: 'plugins/aws.png', |
||||
description: |
||||
'Amazon Simple Email Service (SES) is a cost-effective, flexible, and scalable email service that enables developers to send mail from within any application.', |
||||
price: 'Free', |
||||
tags: 'Email', |
||||
category: 'Email', |
||||
inputs: { |
||||
title: 'Configure Amazon Simple Email Service (SES)', |
||||
items: [ |
||||
{ |
||||
key: 'from', |
||||
label: 'From', |
||||
placeholder: 'From', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'region', |
||||
label: 'Region', |
||||
placeholder: 'Region', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_key', |
||||
label: 'Access Key', |
||||
placeholder: 'Access Key', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_secret', |
||||
label: 'Access Secret', |
||||
placeholder: 'Access Secret', |
||||
type: XcType.Password, |
||||
required: true, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
placeholder: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
placeholder: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: |
||||
'Successfully installed and email notification will use Amazon SES', |
||||
msgOnUninstall: '', |
||||
}, |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,21 @@
|
||||
import axios from 'axios'; |
||||
import type { IWebhookNotificationAdapter } from 'nc-plugin'; |
||||
|
||||
export default class Slack implements IWebhookNotificationAdapter { |
||||
public init(): Promise<any> { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
|
||||
public async sendMessage(text: string, payload: any): Promise<any> { |
||||
for (const { webhook_url } of payload?.channels) { |
||||
try { |
||||
return await axios.post(webhook_url, { |
||||
text, |
||||
}); |
||||
} catch (e) { |
||||
console.log(e); |
||||
throw e; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcWebhookNotificationPlugin } from 'nc-plugin'; |
||||
import Slack from './Slack'; |
||||
import type { IWebhookNotificationAdapter } from 'nc-plugin'; |
||||
|
||||
class SlackPlugin extends XcWebhookNotificationPlugin { |
||||
private static notificationAdapter: Slack; |
||||
|
||||
public getAdapter(): IWebhookNotificationAdapter { |
||||
return SlackPlugin.notificationAdapter; |
||||
} |
||||
|
||||
public async init(_config: any): Promise<any> { |
||||
SlackPlugin.notificationAdapter = new Slack(); |
||||
await SlackPlugin.notificationAdapter.init(); |
||||
} |
||||
} |
||||
|
||||
export default SlackPlugin; |
@ -0,0 +1,56 @@
|
||||
import { XcActionType, XcType } from 'nocodb-sdk'; |
||||
import SlackPlugin from './SlackPlugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: SlackPlugin, |
||||
title: 'Slack', |
||||
version: '0.0.1', |
||||
logo: 'plugins/slack.webp', |
||||
description: |
||||
'Slack brings team communication and collaboration into one place so you can get more work done, whether you belong to a large enterprise or a small business. ', |
||||
price: 'Free', |
||||
tags: 'Chat', |
||||
category: 'Chat', |
||||
inputs: { |
||||
title: 'Configure Slack', |
||||
array: true, |
||||
items: [ |
||||
{ |
||||
key: 'channel', |
||||
label: 'Channel Name', |
||||
placeholder: 'Channel Name', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'webhook_url', |
||||
label: 'Webhook URL', |
||||
placeholder: 'Webhook URL', |
||||
type: XcType.Password, |
||||
required: true, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
placeholder: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
placeholder: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: |
||||
'Successfully installed and Slack is enabled for notification.', |
||||
msgOnUninstall: '', |
||||
}, |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,59 @@
|
||||
import nodemailer from 'nodemailer'; |
||||
import type { IEmailAdapter } from 'nc-plugin'; |
||||
import type Mail from 'nodemailer/lib/mailer'; |
||||
import type { XcEmail } from '../../interface/IEmailAdapter'; |
||||
|
||||
export default class SMTP implements IEmailAdapter { |
||||
private transporter: Mail; |
||||
private input: any; |
||||
|
||||
constructor(input: any) { |
||||
this.input = input; |
||||
} |
||||
|
||||
public async init(): Promise<any> { |
||||
const config = { |
||||
host: this.input?.host, |
||||
port: parseInt(this.input?.port, 10), |
||||
secure: |
||||
typeof this.input?.secure === 'boolean' |
||||
? this.input?.secure |
||||
: this.input?.secure === 'true', |
||||
ignoreTLS: |
||||
typeof this.input?.ignoreTLS === 'boolean' |
||||
? this.input?.ignoreTLS |
||||
: this.input?.ignoreTLS === 'true', |
||||
auth: { |
||||
user: this.input?.username, |
||||
pass: this.input?.password, |
||||
}, |
||||
tls: { |
||||
rejectUnauthorized: this.input?.rejectUnauthorized, |
||||
}, |
||||
}; |
||||
|
||||
this.transporter = nodemailer.createTransport(config); |
||||
} |
||||
|
||||
public async mailSend(mail: XcEmail): Promise<any> { |
||||
if (this.transporter) { |
||||
await this.transporter.sendMail({ ...mail, from: this.input.from }); |
||||
} |
||||
} |
||||
|
||||
public async test(): Promise<boolean> { |
||||
try { |
||||
await this.mailSend({ |
||||
to: this.input.from, |
||||
subject: 'Test email', |
||||
html: 'Test email', |
||||
} as any); |
||||
return true; |
||||
} catch (e) { |
||||
console.log('SMTP test error :: ', e); |
||||
throw new Error( |
||||
'SMTP test failed, please check server log for more details.' |
||||
); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcEmailPlugin } from 'nc-plugin'; |
||||
import SMTP from './SMTP'; |
||||
import type { IEmailAdapter } from 'nc-plugin'; |
||||
|
||||
class SMTPPlugin extends XcEmailPlugin { |
||||
private static storageAdapter: SMTP; |
||||
|
||||
public getAdapter(): IEmailAdapter { |
||||
return SMTPPlugin.storageAdapter; |
||||
} |
||||
|
||||
public async init(config: any): Promise<any> { |
||||
SMTPPlugin.storageAdapter = new SMTP(config); |
||||
await SMTPPlugin.storageAdapter.init(); |
||||
} |
||||
} |
||||
|
||||
export default SMTPPlugin; |
@ -0,0 +1,96 @@
|
||||
import { XcActionType, XcType } from 'nocodb-sdk'; |
||||
import SMTPPlugin from './SMTPPlugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
// @author <dean@deanlofts.xyz>
|
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: SMTPPlugin, |
||||
title: 'SMTP', |
||||
version: '0.0.2', |
||||
// icon: 'mdi-email-outline',
|
||||
description: 'SMTP email client', |
||||
price: 'Free', |
||||
tags: 'Email', |
||||
category: 'Email', |
||||
inputs: { |
||||
title: 'Configure Email SMTP', |
||||
items: [ |
||||
{ |
||||
key: 'from', |
||||
label: 'From', |
||||
placeholder: 'eg: admin@run.com', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'host', |
||||
label: 'Host', |
||||
placeholder: 'eg: smtp.run.com', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'port', |
||||
label: 'Port', |
||||
placeholder: 'Port', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'secure', |
||||
label: 'Secure', |
||||
placeholder: 'Secure', |
||||
type: XcType.Checkbox, |
||||
required: false, |
||||
}, |
||||
{ |
||||
key: 'ignoreTLS', |
||||
label: 'Ignore TLS', |
||||
placeholder: 'Ignore TLS', |
||||
type: XcType.Checkbox, |
||||
required: false, |
||||
}, |
||||
{ |
||||
key: 'rejectUnauthorized', |
||||
label: 'Reject Unauthorized', |
||||
placeholder: 'Reject Unauthorized', |
||||
type: XcType.Checkbox, |
||||
required: false, |
||||
}, |
||||
{ |
||||
key: 'username', |
||||
label: 'Username', |
||||
placeholder: 'Username', |
||||
type: XcType.SingleLineText, |
||||
required: false, |
||||
}, |
||||
{ |
||||
key: 'password', |
||||
label: 'Password', |
||||
placeholder: 'Password', |
||||
type: XcType.Password, |
||||
required: false, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: |
||||
'Successfully installed and email notification will use SMTP configuration', |
||||
msgOnUninstall: '', |
||||
}, |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,140 @@
|
||||
import fs from 'fs'; |
||||
import { promisify } from 'util'; |
||||
import AWS from 'aws-sdk'; |
||||
import request from 'request'; |
||||
import { |
||||
generateTempFilePath, |
||||
waitForStreamClose, |
||||
} from '../../utils/pluginUtils'; |
||||
import type { IStorageAdapterV2, XcFile } from 'nc-plugin'; |
||||
|
||||
export default class Spaces implements IStorageAdapterV2 { |
||||
private s3Client: AWS.S3; |
||||
private input: any; |
||||
|
||||
constructor(input: any) { |
||||
this.input = input; |
||||
} |
||||
|
||||
async fileCreate(key: string, file: XcFile): Promise<any> { |
||||
const uploadParams: any = { |
||||
ACL: 'public-read', |
||||
ContentType: file.mimetype, |
||||
}; |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
const fileStream = fs.createReadStream(file.path); |
||||
fileStream.on('error', (err) => { |
||||
console.log('File Error', err); |
||||
reject(err); |
||||
}); |
||||
|
||||
uploadParams.Body = fileStream; |
||||
uploadParams.Key = key; |
||||
|
||||
// call S3 to retrieve upload file to specified bucket
|
||||
this.s3Client.upload(uploadParams, (err, data) => { |
||||
if (err) { |
||||
console.log('Error', err); |
||||
reject(err); |
||||
} |
||||
if (data) { |
||||
resolve(data.Location); |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
async fileCreateByUrl(key: string, url: string): Promise<any> { |
||||
const uploadParams: any = { |
||||
ACL: 'public-read', |
||||
}; |
||||
return new Promise((resolve, reject) => { |
||||
// Configure the file stream and obtain the upload parameters
|
||||
request( |
||||
{ |
||||
url: url, |
||||
encoding: null, |
||||
}, |
||||
(err, httpResponse, body) => { |
||||
if (err) return reject(err); |
||||
|
||||
uploadParams.Body = body; |
||||
uploadParams.Key = key; |
||||
uploadParams.ContentType = httpResponse.headers['content-type']; |
||||
|
||||
// call S3 to retrieve upload file to specified bucket
|
||||
this.s3Client.upload(uploadParams, (err1, data) => { |
||||
if (err) { |
||||
console.log('Error', err); |
||||
reject(err1); |
||||
} |
||||
if (data) { |
||||
resolve(data.Location); |
||||
} |
||||
}); |
||||
} |
||||
); |
||||
}); |
||||
} |
||||
|
||||
public async fileDelete(_path: string): Promise<any> { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
|
||||
public async fileRead(key: string): Promise<any> { |
||||
return new Promise((resolve, reject) => { |
||||
this.s3Client.getObject({ Key: key } as any, (err, data) => { |
||||
if (err) { |
||||
return reject(err); |
||||
} |
||||
if (!data?.Body) { |
||||
return reject(data); |
||||
} |
||||
return resolve(data.Body); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
public async init(): Promise<any> { |
||||
// const s3Options: any = {
|
||||
// params: {Bucket: process.env.NC_S3_BUCKET},
|
||||
// region: process.env.NC_S3_REGION
|
||||
// };
|
||||
//
|
||||
// s3Options.accessKeyId = process.env.NC_S3_KEY;
|
||||
// s3Options.secretAccessKey = process.env.NC_S3_SECRET;
|
||||
|
||||
const s3Options: any = { |
||||
params: { Bucket: this.input.bucket }, |
||||
region: this.input.region, |
||||
}; |
||||
|
||||
s3Options.accessKeyId = this.input.access_key; |
||||
s3Options.secretAccessKey = this.input.access_secret; |
||||
|
||||
s3Options.endpoint = new AWS.Endpoint( |
||||
`${this.input.region || 'nyc3'}.digitaloceanspaces.com` |
||||
); |
||||
|
||||
this.s3Client = new AWS.S3(s3Options); |
||||
} |
||||
|
||||
public async test(): Promise<boolean> { |
||||
try { |
||||
const tempFile = generateTempFilePath(); |
||||
const createStream = fs.createWriteStream(tempFile); |
||||
await waitForStreamClose(createStream); |
||||
await this.fileCreate('nc-test-file.txt', { |
||||
path: tempFile, |
||||
mimetype: 'text/plain', |
||||
originalname: 'temp.txt', |
||||
size: '', |
||||
}); |
||||
await promisify(fs.unlink)(tempFile); |
||||
return true; |
||||
} catch (e) { |
||||
throw e; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcStoragePlugin } from 'nc-plugin'; |
||||
import Spaces from './Spaces'; |
||||
import type { IStorageAdapterV2 } from 'nc-plugin'; |
||||
|
||||
class SpacesPlugin extends XcStoragePlugin { |
||||
private static storageAdapter: Spaces; |
||||
|
||||
public getAdapter(): IStorageAdapterV2 { |
||||
return SpacesPlugin.storageAdapter; |
||||
} |
||||
|
||||
public async init(config: any): Promise<any> { |
||||
SpacesPlugin.storageAdapter = new Spaces(config); |
||||
await SpacesPlugin.storageAdapter.init(); |
||||
} |
||||
} |
||||
|
||||
export default SpacesPlugin; |
@ -0,0 +1,69 @@
|
||||
import { XcActionType, XcType } from 'nocodb-sdk'; |
||||
import SpacesPlugin from './SpacesPlugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: SpacesPlugin, |
||||
title: 'Spaces', |
||||
version: '0.0.1', |
||||
logo: 'plugins/spaces.png', |
||||
description: |
||||
'Store & deliver vast amounts of content with a simple architecture.', |
||||
price: 'Free', |
||||
tags: 'Storage', |
||||
category: 'Storage', |
||||
inputs: { |
||||
title: 'DigitalOcean Spaces', |
||||
items: [ |
||||
{ |
||||
key: 'bucket', |
||||
label: 'Bucket Name', |
||||
placeholder: 'Bucket Name', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'region', |
||||
label: 'Region', |
||||
placeholder: 'Region', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_key', |
||||
label: 'Access Key', |
||||
placeholder: 'Access Key', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'access_secret', |
||||
label: 'Access Secret', |
||||
placeholder: 'Access Secret', |
||||
type: XcType.Password, |
||||
required: true, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
placeholder: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
placeholder: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: |
||||
'Successfully installed and attachment will be stored in DigitalOcean Spaces', |
||||
msgOnUninstall: '', |
||||
}, |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,91 @@
|
||||
import fs from 'fs'; |
||||
import path from 'path'; |
||||
import { promisify } from 'util'; |
||||
import mkdirp from 'mkdirp'; |
||||
import axios from 'axios'; |
||||
import type { IStorageAdapterV2, XcFile } from 'nc-plugin'; |
||||
import NcConfigFactory from 'src/utils/NcConfigFactory'; |
||||
|
||||
export default class Local implements IStorageAdapterV2 { |
||||
constructor() {} |
||||
|
||||
public async fileCreate(key: string, file: XcFile): Promise<any> { |
||||
const destPath = path.join(NcConfigFactory.getToolDir(), ...key.split('/')); |
||||
try { |
||||
await mkdirp(path.dirname(destPath)); |
||||
const data = await promisify(fs.readFile)(file.path); |
||||
await promisify(fs.writeFile)(destPath, data); |
||||
await promisify(fs.unlink)(file.path); |
||||
// await fs.promises.rename(file.path, destPath);
|
||||
} catch (e) { |
||||
throw e; |
||||
} |
||||
} |
||||
|
||||
async fileCreateByUrl(key: string, url: string): Promise<any> { |
||||
const destPath = path.join(NcConfigFactory.getToolDir(), ...key.split('/')); |
||||
return new Promise((resolve, reject) => { |
||||
axios |
||||
.get(url, { |
||||
responseType: 'stream', |
||||
headers: { |
||||
accept: |
||||
'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', |
||||
'accept-language': 'en-US,en;q=0.9', |
||||
'cache-control': 'no-cache', |
||||
pragma: 'no-cache', |
||||
'user-agent': |
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36', |
||||
origin: 'https://www.airtable.com/', |
||||
}, |
||||
}) |
||||
.then(async (response) => { |
||||
await mkdirp(path.dirname(destPath)); |
||||
const file = fs.createWriteStream(destPath); |
||||
// close() is async, call cb after close completes
|
||||
file.on('finish', () => { |
||||
file.close((err) => { |
||||
if (err) { |
||||
return reject(err); |
||||
} |
||||
resolve(null); |
||||
}); |
||||
}); |
||||
|
||||
file.on('error', (err) => { |
||||
// Handle errors
|
||||
fs.unlink(destPath, () => reject(err.message)); // delete the (partial) file and then return the error
|
||||
}); |
||||
|
||||
response.data.pipe(file); |
||||
}) |
||||
.catch((err) => { |
||||
reject(err.message); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
// todo: implement
|
||||
fileDelete(_path: string): Promise<any> { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
|
||||
public async fileRead(filePath: string): Promise<any> { |
||||
try { |
||||
const fileData = await fs.promises.readFile( |
||||
path.join(NcConfigFactory.getToolDir(), ...filePath.split('/')) |
||||
); |
||||
return fileData; |
||||
} catch (e) { |
||||
throw e; |
||||
} |
||||
} |
||||
|
||||
init(): Promise<any> { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
|
||||
test(): Promise<boolean> { |
||||
return Promise.resolve(false); |
||||
} |
||||
} |
@ -0,0 +1,21 @@
|
||||
import axios from 'axios'; |
||||
import type { IWebhookNotificationAdapter } from 'nc-plugin'; |
||||
|
||||
export default class Teams implements IWebhookNotificationAdapter { |
||||
public init(): Promise<any> { |
||||
return Promise.resolve(undefined); |
||||
} |
||||
|
||||
public async sendMessage(Text: string, payload: any): Promise<any> { |
||||
for (const { webhook_url } of payload?.channels) { |
||||
try { |
||||
return await axios.post(webhook_url, { |
||||
Text, |
||||
}); |
||||
} catch (e) { |
||||
console.log(e); |
||||
throw e; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,18 @@
|
||||
import { XcWebhookNotificationPlugin } from 'nc-plugin'; |
||||
import Teams from './Teams'; |
||||
import type { IWebhookNotificationAdapter } from 'nc-plugin'; |
||||
|
||||
class TeamsPlugin extends XcWebhookNotificationPlugin { |
||||
private static notificationAdapter: Teams; |
||||
|
||||
public getAdapter(): IWebhookNotificationAdapter { |
||||
return TeamsPlugin.notificationAdapter; |
||||
} |
||||
|
||||
public async init(_config: any): Promise<any> { |
||||
TeamsPlugin.notificationAdapter = new Teams(); |
||||
await TeamsPlugin.notificationAdapter.init(); |
||||
} |
||||
} |
||||
|
||||
export default TeamsPlugin; |
@ -0,0 +1,56 @@
|
||||
import { XcActionType, XcType } from 'nocodb-sdk'; |
||||
import TeamsPlugin from './TeamsPlugin'; |
||||
import type { XcPluginConfig } from 'nc-plugin'; |
||||
|
||||
const config: XcPluginConfig = { |
||||
builder: TeamsPlugin, |
||||
title: 'Microsoft Teams', |
||||
version: '0.0.1', |
||||
logo: 'plugins/teams.ico', |
||||
description: |
||||
'Microsoft Teams is for everyone · Instantly go from group chat to video call with the touch of a button.', |
||||
price: 'Free', |
||||
tags: 'Chat', |
||||
category: 'Chat', |
||||
inputs: { |
||||
title: 'Configure Microsoft Teams', |
||||
array: true, |
||||
items: [ |
||||
{ |
||||
key: 'channel', |
||||
label: 'Channel Name', |
||||
placeholder: 'Channel Name', |
||||
type: XcType.SingleLineText, |
||||
required: true, |
||||
}, |
||||
{ |
||||
key: 'webhook_url', |
||||
label: 'Webhook URL', |
||||
placeholder: 'Webhook URL', |
||||
type: XcType.Password, |
||||
required: true, |
||||
}, |
||||
], |
||||
actions: [ |
||||
{ |
||||
label: 'Test', |
||||
placeholder: 'Test', |
||||
key: 'test', |
||||
actionType: XcActionType.TEST, |
||||
type: XcType.Button, |
||||
}, |
||||
{ |
||||
label: 'Save', |
||||
placeholder: 'Save', |
||||
key: 'save', |
||||
actionType: XcActionType.SUBMIT, |
||||
type: XcType.Button, |
||||
}, |
||||
], |
||||
msgOnInstall: |
||||
'Successfully installed and Microsoft Teams is enabled for notification.', |
||||
msgOnUninstall: '', |
||||
}, |
||||
}; |
||||
|
||||
export default config; |
@ -0,0 +1,30 @@
|
||||
import twilio from 'twilio'; |
||||
import type { IWebhookNotificationAdapter } from 'nc-plugin'; |
||||
|
||||
export default class Twilio implements IWebhookNotificationAdapter { |
||||
private input: any; |
||||
private client: any; |
||||
|
||||
constructor(input: any) { |
||||
this.input = input; |
||||
} |
||||
|
||||
public async init() { |
||||
this.client = twilio(this.input.sid, this.input.token); |
||||
} |
||||
|
||||
public async sendMessage(content: string, payload: any): Promise<any> { |
||||
for (const num of payload?.to?.split(/\s*?,\s*?/)) { |
||||
try { |
||||
await this.client.messages.create({ |
||||
body: content, |
||||
from: this.input.from, |
||||
to: num, |
||||
}); |
||||
} catch (e) { |
||||
console.log(e); |
||||
throw e; |
||||
} |
||||
} |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue