| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import UITypes from './UITypes'; | ||
|
|
||
| export interface Column { | ||
| column_name: string; | ||
| ref_column_name: string; | ||
| uidt?: UITypes; | ||
| dtxp?: any; | ||
| dt?: any; | ||
| } | ||
| export interface Table { | ||
| table_name: string; | ||
| ref_table_name: string; | ||
| columns: Array<Column>; | ||
| } | ||
| export interface Template { | ||
| title: string; | ||
| tables: Array<Table>; | ||
| } | ||
|
|
||
| export default abstract class TemplateGenerator { | ||
| abstract parse(): Promise<any>; | ||
| abstract parseTemplate(): Promise<Template>; | ||
| abstract getColumns(): Promise<any>; | ||
| abstract parseData(): Promise<any>; | ||
| abstract getData(): Promise<{ | ||
| [table_name: string]: Array<{ | ||
| [key: string]: any; | ||
| }>; | ||
| }>; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| export function validatePassword(p) { | ||
| let error = ''; | ||
| let progress = 0; | ||
| let hint = null; | ||
| let valid = true; | ||
| if (!p) { | ||
| error = | ||
| 'At least 8 letters with one Uppercase, one number and one special letter'; | ||
| valid = false; | ||
| } else { | ||
| if (!(p.length >= 8)) { | ||
| error += 'Atleast 8 letters. '; | ||
| valid = false; | ||
| } else { | ||
| progress = Math.min(100, progress + 25); | ||
| } | ||
|
|
||
| if (!p.match(/.*[A-Z].*/)) { | ||
| error += 'One Uppercase Letter. '; | ||
| valid = false; | ||
| } else { | ||
| progress = Math.min(100, progress + 25); | ||
| } | ||
|
|
||
| if (!p.match(/.*[0-9].*/)) { | ||
| error += 'One Number. '; | ||
| valid = false; | ||
| } else { | ||
| progress = Math.min(100, progress + 25); | ||
| } | ||
|
|
||
| if (!p.match(/[$&+,:;=?@#|'<>.^*()%!_-]/)) { | ||
| error += 'One special letter. '; | ||
| hint = "Allowed special character list : $&+,:;=?@#|'<>.^*()%!_-"; | ||
| valid = false; | ||
| } else { | ||
| progress = Math.min(100, progress + 25); | ||
| } | ||
| } | ||
| return { error, valid, progress, hint }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| export function sanitize(v) { | ||
| return v?.replace(/([^\\]|^)(\?+)/g, (_, m1, m2) => { | ||
| return `${m1}${m2.split('?').join('\\?')}`; | ||
| }); | ||
| } | ||
|
|
||
| export function unsanitize(v) { | ||
| return v?.replace(/\\[?]/g, '?'); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| import User from '../../../models/User'; | ||
| import { v4 as uuidv4 } from 'uuid'; | ||
| import { promisify } from 'util'; | ||
| import { Tele } from 'nc-help'; | ||
|
|
||
| import bcrypt from 'bcryptjs'; | ||
| import Noco from '../../../Noco'; | ||
| import { CacheScope, MetaTable } from '../../../utils/globals'; | ||
| import ProjectUser from '../../../models/ProjectUser'; | ||
| import { validatePassword } from 'nocodb-sdk'; | ||
| import boxen from 'boxen'; | ||
| import NocoCache from '../../../cache/NocoCache'; | ||
|
|
||
| const { isEmail } = require('validator'); | ||
| const rolesLevel = { owner: 0, creator: 1, editor: 2, commenter: 3, viewer: 4 }; | ||
|
|
||
| export default async function initAdminFromEnv(_ncMeta = Noco.ncMeta) { | ||
| if (process.env.NC_ADMIN_EMAIL && process.env.NC_ADMIN_PASSWORD) { | ||
| if (!isEmail(process.env.NC_ADMIN_EMAIL?.trim())) { | ||
| console.log( | ||
| '\n', | ||
| boxen( | ||
| `Provided admin email '${process.env.NC_ADMIN_EMAIL}' is not valid`, | ||
| { | ||
| title: 'Invalid admin email', | ||
| padding: 1, | ||
| borderStyle: 'double', | ||
| titleAlignment: 'center', | ||
| borderColor: 'red' | ||
| } | ||
| ), | ||
| '\n' | ||
| ); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const { valid, error, hint } = validatePassword( | ||
| process.env.NC_ADMIN_PASSWORD | ||
| ); | ||
| if (!valid) { | ||
| console.log( | ||
| '\n', | ||
| boxen(`${error}${hint ? `\n\n${hint}` : ''}`, { | ||
| title: 'Invalid admin password', | ||
| padding: 1, | ||
| borderStyle: 'double', | ||
| titleAlignment: 'center', | ||
| borderColor: 'red' | ||
| }), | ||
| '\n' | ||
| ); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| let ncMeta; | ||
| try { | ||
| ncMeta = await _ncMeta.startTransaction(); | ||
| const email = process.env.NC_ADMIN_EMAIL.toLowerCase().trim(); | ||
|
|
||
| const salt = await promisify(bcrypt.genSalt)(10); | ||
| const password = await promisify(bcrypt.hash)( | ||
| process.env.NC_ADMIN_PASSWORD, | ||
| salt | ||
| ); | ||
| const email_verification_token = uuidv4(); | ||
|
|
||
| // if super admin not present | ||
| if (await User.isFirst(ncMeta)) { | ||
| const roles = 'user,super'; | ||
|
|
||
| // roles = 'owner,creator,editor' | ||
| Tele.emit('evt', { | ||
| evt_type: 'project:invite', | ||
| count: 1 | ||
| }); | ||
|
|
||
| await User.insert( | ||
| { | ||
| firstname: '', | ||
| lastname: '', | ||
| email, | ||
| salt, | ||
| password, | ||
| email_verification_token, | ||
| roles | ||
| }, | ||
| ncMeta | ||
| ); | ||
| } else { | ||
| const salt = await promisify(bcrypt.genSalt)(10); | ||
| const password = await promisify(bcrypt.hash)( | ||
| process.env.NC_ADMIN_PASSWORD, | ||
| salt | ||
| ); | ||
| const email_verification_token = uuidv4(); | ||
| const superUser = await ncMeta.metaGet2(null, null, MetaTable.USERS, { | ||
| roles: 'user,super' | ||
| }); | ||
|
|
||
| if (email !== superUser.email) { | ||
| // update admin email and password and migrate projects | ||
| // if user already present and associated with some project | ||
|
|
||
| // check user account already present with the new admin email | ||
| const existingUserWithNewEmail = await User.getByEmail(email, ncMeta); | ||
|
|
||
| if (existingUserWithNewEmail?.id) { | ||
| // get all project access belongs to the existing account | ||
| // and migrate to the admin account | ||
| const existingUserProjects = await ncMeta.metaList2( | ||
| null, | ||
| null, | ||
| MetaTable.PROJECT_USERS, | ||
| { | ||
| condition: { fk_user_id: existingUserWithNewEmail.id } | ||
| } | ||
| ); | ||
|
|
||
| for (const existingUserProject of existingUserProjects) { | ||
| const userProject = await ProjectUser.get( | ||
| existingUserProject.project_id, | ||
| superUser.id, | ||
| ncMeta | ||
| ); | ||
|
|
||
| // if admin user already have access to the project | ||
| // then update role based on the highest access level | ||
| if (userProject) { | ||
| if ( | ||
| rolesLevel[userProject.roles] > | ||
| rolesLevel[existingUserProject.roles] | ||
| ) { | ||
| await ProjectUser.update( | ||
| userProject.project_id, | ||
| superUser.id, | ||
| existingUserProject.roles, | ||
| ncMeta | ||
| ); | ||
| } | ||
| } else { | ||
| // if super doesn't have access then add the access | ||
| await ProjectUser.insert( | ||
| { | ||
| ...existingUserProject, | ||
| fk_user_id: superUser.id | ||
| }, | ||
| ncMeta | ||
| ); | ||
| } | ||
| // delete the old project access entry from DB | ||
| await ProjectUser.delete( | ||
| existingUserProject.project_id, | ||
| existingUserProject.fk_user_id, | ||
| ncMeta | ||
| ); | ||
| } | ||
|
|
||
| // delete existing user | ||
| await ncMeta.metaDelete( | ||
| null, | ||
| null, | ||
| MetaTable.USERS, | ||
| existingUserWithNewEmail.id | ||
| ); | ||
|
|
||
| // clear cache | ||
| await NocoCache.delAll( | ||
| CacheScope.USER, | ||
| `${existingUserWithNewEmail.email}___*` | ||
| ); | ||
| await NocoCache.del( | ||
| `${CacheScope.USER}:${existingUserWithNewEmail.id}` | ||
| ); | ||
| await NocoCache.del( | ||
| `${CacheScope.USER}:${existingUserWithNewEmail.email}` | ||
| ); | ||
|
|
||
| // Update email and password of super admin account | ||
| await User.update( | ||
| superUser.id, | ||
| { | ||
| salt, | ||
| email, | ||
| password, | ||
| email_verification_token, | ||
| token_version: null, | ||
| refresh_token: null | ||
| }, | ||
| ncMeta | ||
| ); | ||
| } else { | ||
| // if email's are not different update the password and hash | ||
| await User.update( | ||
| superUser.id, | ||
| { | ||
| salt, | ||
| email, | ||
| password, | ||
| email_verification_token, | ||
| token_version: null, | ||
| refresh_token: null | ||
| }, | ||
| ncMeta | ||
| ); | ||
| } | ||
| } else { | ||
| const newPasswordHash = await promisify(bcrypt.hash)( | ||
| process.env.NC_ADMIN_PASSWORD, | ||
| superUser.salt | ||
| ); | ||
|
|
||
| if (newPasswordHash !== superUser.password) { | ||
| // if email's are same and passwords are different | ||
| // then update the password and token version | ||
| await User.update( | ||
| superUser.id, | ||
| { | ||
| salt, | ||
| password, | ||
| email_verification_token, | ||
| token_version: null, | ||
| refresh_token: null | ||
| }, | ||
| ncMeta | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| await ncMeta.commit(); | ||
| } catch (e) { | ||
| console.log('Error occurred while updating/creating admin user'); | ||
| console.log(e); | ||
| await ncMeta.rollback(e); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import crypto from 'crypto'; | ||
|
|
||
| export function randomTokenString(): string { | ||
| return crypto.randomBytes(40).toString('hex'); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ export default class Filter { | |
| comparison_op?: | ||
| | 'eq' | ||
| | 'neq' | ||
| | 'not' | ||
| | 'like' | ||
| | 'nlike' | ||
| | 'empty' | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| ## config.json | ||
| { | ||
| "srcProject": "sample", | ||
| "dstProject": "sample-copy", | ||
| "baseURL": "http://localhost:8080", | ||
| "xc-auth": "Copy Auth Token" | ||
| } | ||
| - baseURL & xc-auth are common configurations for both import & export | ||
|
|
||
| ## Export | ||
| - `srcProject`: specify source project name to be exported. | ||
| - Export JSON file will be created as `srcProject.json` | ||
| - execute | ||
| `cd packages/nocodb/tests/export-import` | ||
| `node exportSchema.js` | ||
|
|
||
| ## Import | ||
| - `srcProject`: specify JSON file name to be imported (sans .JSON suffix) | ||
| - `dstProject`: new project name to be imported as | ||
| - Data will also be imported if `srcProject` exists in NocoDB. Note that, data import isn't via exported JSON | ||
| - execute | ||
| `cd packages/nocodb/tests/export-import` | ||
| `node importSchema.js` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "srcProject": "sample", | ||
| "dstProject": "sample-copy", | ||
| "baseURL": "http://localhost:8080", | ||
| "xc-auth": "Copy Auth Token" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,297 @@ | ||
| const Api = require('nocodb-sdk').Api; | ||
| const { UITypes } = require('nocodb-sdk'); | ||
| const jsonfile = require('jsonfile'); | ||
|
|
||
| const GRID = 3, GALLERY = 2, FORM = 1; | ||
|
|
||
| let ncMap = { /* id: name <string> */ }; | ||
| let tblSchema = []; | ||
| let api = {}; | ||
| let viewStore = { columns: {}, sort: {}, filter: {} }; | ||
|
|
||
| let inputConfig = jsonfile.readFileSync(`config.json`) | ||
| let ncConfig = { | ||
| projectName: inputConfig.srcProject, | ||
| baseURL: inputConfig.baseURL, | ||
| headers: { | ||
| 'xc-auth': `${inputConfig["xc-auth"]}` | ||
| } | ||
| }; | ||
|
|
||
|
|
||
| // helper routines | ||
| // remove objects containing 0/ false/ null | ||
| // fixme: how to handle when cdf (default value) is configured as 0/ null/ false | ||
| function removeEmpty(obj) { | ||
| return Object.fromEntries( | ||
| Object.entries(obj) | ||
| .filter(([_, v]) => v != null && v != 0 && v != false) | ||
| .map(([k, v]) => [k, v === Object(v) ? removeEmpty(v) : v]) | ||
| ); | ||
| } | ||
|
|
||
| function addColumnSpecificData(c) { | ||
| // pick required fields to proceed further | ||
| let col = removeEmpty( | ||
| (({ id, title, column_name, uidt, dt, pk, pv, rqd, dtxp, system }) => ({ | ||
| id, | ||
| title, | ||
| column_name, | ||
| uidt, | ||
| dt, | ||
| pk, | ||
| pv, | ||
| rqd, | ||
| dtxp, | ||
| system | ||
| }))(c) | ||
| ); | ||
|
|
||
| switch (c.uidt) { | ||
| case UITypes.Formula: | ||
| col.formula = c.colOptions.formula; | ||
| col.formula_raw = c.colOptions.formula_raw; | ||
| break; | ||
| case UITypes.LinkToAnotherRecord: | ||
| col[`colOptions`] = { | ||
| fk_model_id: c.fk_model_id, | ||
| fk_related_model_id: c.colOptions.fk_related_model_id, | ||
| fk_child_column_id: c.colOptions.fk_child_column_id, | ||
| fk_parent_column_id: c.colOptions.fk_parent_column_id, | ||
| type: c.colOptions.type | ||
| }; | ||
| break; | ||
| case UITypes.Lookup: | ||
| col[`colOptions`] = { | ||
| fk_model_id: c.fk_model_id, | ||
| fk_relation_column_id: c.colOptions.fk_relation_column_id, | ||
| fk_lookup_column_id: c.colOptions.fk_lookup_column_id | ||
| }; | ||
| break; | ||
| case UITypes.Rollup: | ||
| col[`colOptions`] = { | ||
| fk_model_id: c.fk_model_id, | ||
| fk_relation_column_id: c.colOptions.fk_relation_column_id, | ||
| fk_rollup_column_id: c.colOptions.fk_rollup_column_id, | ||
| rollup_function: c.colOptions.rollup_function | ||
| }; | ||
| break; | ||
| } | ||
|
|
||
| return col; | ||
| } | ||
|
|
||
| function addViewDetails(v) { | ||
| // pick required fields to proceed further | ||
| let view = (({ id, title, type, show_system_fields, lock_type, order }) => ({ | ||
| id, | ||
| title, | ||
| type, | ||
| show_system_fields, | ||
| lock_type, | ||
| order | ||
| }))(v); | ||
|
|
||
| // form view | ||
| if (v.type === FORM) { | ||
| view.property = (({ | ||
| heading, | ||
| subheading, | ||
| success_msg, | ||
| redirect_after_secs, | ||
| email, | ||
| submit_another_form, | ||
| show_blank_form | ||
| }) => ({ | ||
| heading, | ||
| subheading, | ||
| success_msg, | ||
| redirect_after_secs, | ||
| email, | ||
| submit_another_form, | ||
| show_blank_form | ||
| }))(v.view); | ||
| } | ||
|
|
||
| // gallery view | ||
| else if (v.type === GALLERY) { | ||
| view.property = { | ||
| fk_cover_image_col_id: ncMap[v.view.fk_cover_image_col_id] | ||
| }; | ||
| } | ||
|
|
||
| // gallery view doesn't share column information in api yet | ||
| if (v.type !== GALLERY) { | ||
| if (v.type === GRID) | ||
| view.columns = viewStore.columns[v.id].map(a => | ||
| (({ id, width, order, show }) => ({ id, width, order, show }))(a) | ||
| ); | ||
| if (v.type === FORM) | ||
| view.columns = viewStore.columns[v.id].map(a => | ||
| (({ id, order, show, label, help, description, required }) => ({ | ||
| id, | ||
| order, | ||
| show, | ||
| label, | ||
| help, | ||
| description, | ||
| required | ||
| }))(a) | ||
| ); | ||
|
|
||
| for (let i = 0; i < view.columns?.length; i++) | ||
| view.columns[i].title = ncMap[viewStore.columns[v.id][i].id]; | ||
|
|
||
| // skip hm & mm columns | ||
| view.columns = view.columns | ||
| ?.filter(a => a.title?.includes('_nc_m2m_') === false) | ||
| .filter(a => a.title?.includes('nc_') === false); | ||
| } | ||
|
|
||
| // filter & sort configurations | ||
| if (v.type !== FORM) { | ||
| view.sort = viewStore.sort[v.id].map(a => | ||
| (({ fk_column_id, direction, order }) => ({ | ||
| fk_column_id, | ||
| direction, | ||
| order | ||
| }))(a) | ||
| ); | ||
| view.filter = viewStore.filter[v.id].map(a => | ||
| (({ fk_column_id, logical_op, comparison_op, value, order }) => ({ | ||
| fk_column_id, | ||
| logical_op, | ||
| comparison_op, | ||
| value, | ||
| order | ||
| }))(a) | ||
| ); | ||
| } | ||
| return view; | ||
| } | ||
|
|
||
| // view data stored as is for quick access | ||
| async function storeViewDetails(tableId) { | ||
| // read view data for each table | ||
| let viewList = await api.dbView.list(tableId); | ||
| for (let j = 0; j < viewList.list.length; j++) { | ||
| let v = viewList.list[j]; | ||
| let viewDetails = []; | ||
|
|
||
| // invoke view specific read to populate columns information | ||
| if (v.type === FORM) viewDetails = (await api.dbView.formRead(v.id)).columns; | ||
| else if (v.type === GALLERY) viewDetails = await api.dbView.galleryRead(v.id); | ||
| else if (v.type === GRID) viewDetails = await api.dbView.gridColumnsList(v.id); | ||
| viewStore.columns[v.id] = viewDetails; | ||
|
|
||
| // populate sort information | ||
| let vSort = await api.dbTableSort.list(v.id); | ||
| viewStore.sort[v.id] = vSort.sorts.list; | ||
|
|
||
| let vFilter = await api.dbTableFilter.read(v.id); | ||
| viewStore.filter[v.id] = vFilter; | ||
| } | ||
| } | ||
|
|
||
| // mapping table for quick information access | ||
| // store maps for tableId, columnId, viewColumnId & viewId to their names | ||
| async function generateMapTbl(pId) { | ||
| const tblList = await api.dbTable.list(pId); | ||
|
|
||
| for (let i = 0; i < tblList.list.length; i++) { | ||
| let tblId = tblList.list[i].id; | ||
| let tbl = await api.dbTable.read(tblId); | ||
|
|
||
| // table ID <> name | ||
| ncMap[tblId] = tbl.title; | ||
|
|
||
| // column ID <> name | ||
| tbl.columns.map(x => (ncMap[x.id] = x.title)); | ||
|
|
||
| // view ID <> name | ||
| tbl.views.map(x => (ncMap[x.id] = x.tn)); | ||
|
|
||
| for (let i = 0; i < tbl.views.length; i++) { | ||
| let x = tbl.views[i]; | ||
| let viewColumns = []; | ||
| if (x.type === FORM) viewColumns = (await api.dbView.formRead(x.id)).columns; | ||
| else if (x.type === GALLERY) | ||
| viewColumns = (await api.dbView.galleryRead(x.id)).columns; | ||
| else if (x.type === GRID) viewColumns = await api.dbView.gridColumnsList(x.id); | ||
|
|
||
| // view column ID <> name | ||
| viewColumns?.map(a => (ncMap[a.id] = ncMap[a.fk_column_id])); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // main | ||
| // | ||
| async function exportSchema() { | ||
| api = new Api(ncConfig); | ||
|
|
||
| // fetch project details (id et.al) | ||
| const x = await api.project.list(); | ||
| const p = x.list.find(a => a.title === ncConfig.projectName); | ||
|
|
||
| await generateMapTbl(p.id); | ||
|
|
||
| // read project | ||
| const tblList = await api.dbTable.list(p.id); | ||
|
|
||
| // for each table | ||
| for (let i = 0; i < tblList.list.length; i++) { | ||
| let tblId = tblList.list[i].id; | ||
| await storeViewDetails(tblId); | ||
|
|
||
| let tbl = await api.dbTable.read(tblId); | ||
|
|
||
| // prepare schema | ||
| let tSchema = { | ||
| id: tbl.id, | ||
| title: tbl.title, | ||
| table_name: tbl?.table_name, | ||
| columns: [...tbl.columns.map(c => addColumnSpecificData(c))] | ||
| .filter(a => a.title.includes('_nc_m2m_') === false) // mm | ||
| .filter(a => a.title.includes(p.prefix) === false) // hm | ||
| .filter( | ||
| a => !(a?.system === 1 && a.uidt === UITypes.LinkToAnotherRecord) | ||
| ), | ||
| views: [...tbl.views.map(v => addViewDetails(v))] | ||
| }; | ||
| tblSchema.push(tSchema); | ||
| } | ||
| } | ||
|
|
||
| (async () => { | ||
| await exportSchema(); | ||
| jsonfile.writeFileSync( | ||
| `${ncConfig.projectName.replace(/ /g, '_')}.json`, | ||
| tblSchema, | ||
| { spaces: 2 } | ||
| ); | ||
| })().catch(e => { | ||
| console.log(e); | ||
| }); | ||
|
|
||
| /** | ||
| * @copyright Copyright (c) 2021, Xgene Cloud Ltd | ||
| * | ||
| * @author Raju Udava <sivadstala@gmail.com> | ||
| * | ||
| * @license GNU AGPL version 3 or any later version | ||
| * | ||
| * This program is free software: you can redistribute it and/or modify | ||
| * it under the terms of the GNU Affero General Public License as | ||
| * published by the Free Software Foundation, either version 3 of the | ||
| * License, or (at your option) any later version. | ||
| * | ||
| * This program is distributed in the hope that it will be useful, | ||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| * GNU Affero General Public License for more details. | ||
| * | ||
| * You should have received a copy of the GNU Affero General Public License | ||
| * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
| * | ||
| */ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| version: "2.1" | ||
|
|
||
| services: | ||
| pg96: | ||
| image: postgres:9.6 | ||
| restart: always | ||
| environment: | ||
| POSTGRES_PASSWORD: password | ||
| ports: | ||
| - 5432:5432 | ||
| volumes: | ||
| - ../../packages/nocodb/tests/pg-cy-quick:/docker-entrypoint-initdb.d | ||
| healthcheck: | ||
| test: ["CMD-SHELL", "pg_isready -U postgres"] | ||
| interval: 10s | ||
| timeout: 5s | ||
| retries: 5 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,275 @@ | ||
| import { mainPage } from "../../support/page_objects/mainPage"; | ||
| import { | ||
| isTestSuiteActive, | ||
| } from "../../support/page_objects/projectConstants"; | ||
|
|
||
| export const genTest = (apiType, dbType) => { | ||
| if (!isTestSuiteActive(apiType, dbType)) return; | ||
|
|
||
| describe(`${apiType.toUpperCase()} api - DURATION`, () => { | ||
| const tableName = "DurationTable"; | ||
|
|
||
| // to retrieve few v-input nodes from their label | ||
| // | ||
| const fetchParentFromLabel = (label) => { | ||
| cy.get("label").contains(label).parents(".v-input").click(); | ||
| }; | ||
|
|
||
| // Run once before test- create table | ||
| // | ||
| before(() => { | ||
| mainPage.tabReset(); | ||
| cy.createTable(tableName); | ||
| }); | ||
|
|
||
| after(() => { | ||
| cy.deleteTable(tableName); | ||
| }); | ||
|
|
||
| // Routine to create a new look up column | ||
| // | ||
| const addDurationColumn = (columnName, durationFormat) => { | ||
| // (+) icon at end of column header (to add a new column) | ||
| // opens up a pop up window | ||
| // | ||
| cy.get(".new-column-header").click(); | ||
|
|
||
| // Column name | ||
| cy.get(".nc-column-name-input input").clear().type(`${columnName}`); | ||
|
|
||
| // Column data type | ||
| cy.get(".nc-ui-dt-dropdown").click(); | ||
| cy.getActiveMenu().contains("Duration").click(); | ||
|
|
||
| // Configure Child table & column names | ||
| fetchParentFromLabel("Duration Format"); | ||
| cy.getActiveMenu().contains(durationFormat).click(); | ||
|
|
||
| // click on Save | ||
| cy.get(".nc-col-create-or-edit-card").contains("Save").click(); | ||
|
|
||
| // Verify if column exists. | ||
| // | ||
| cy.get(`th:contains(${columnName})`).should("exist"); | ||
| }; | ||
|
|
||
| // routine to delete column | ||
| // | ||
| const deleteColumnByName = (columnName) => { | ||
| // verify if column exists before delete | ||
| cy.get(`th:contains(${columnName})`).should("exist"); | ||
|
|
||
| // delete opiton visible on mouse-over | ||
| cy.get(`th:contains(${columnName}) .mdi-menu-down`) | ||
| .trigger("mouseover") | ||
| .click(); | ||
|
|
||
| // delete/ confirm on pop-up | ||
| cy.get(".nc-column-delete").click(); | ||
| cy.getActiveModal().find("button:contains(Confirm)").click(); | ||
|
|
||
| // validate if deleted (column shouldnt exist) | ||
| cy.get(`th:contains(${columnName})`).should("not.exist"); | ||
| }; | ||
|
|
||
| // routine to edit column | ||
| // | ||
| const editColumnByName = (oldName, newName, newDurationFormat) => { | ||
| // verify if column exists before delete | ||
| cy.get(`th:contains(${oldName})`).should("exist"); | ||
|
|
||
| // delete opiton visible on mouse-over | ||
| cy.get(`th:contains(${oldName}) .mdi-menu-down`) | ||
| .trigger("mouseover") | ||
| .click(); | ||
|
|
||
| // edit/ save on pop-up | ||
| cy.get(".nc-column-edit").click(); | ||
| cy.get(".nc-column-name-input input").clear().type(newName); | ||
|
|
||
| // Configure Child table & column names | ||
| fetchParentFromLabel("Duration Format"); | ||
| cy.getActiveMenu().contains(newDurationFormat).click(); | ||
|
|
||
| cy.get(".nc-col-create-or-edit-card") | ||
| .contains("Save") | ||
| .click({ force: true }); | ||
|
|
||
| cy.toastWait("Duration column updated successfully"); | ||
|
|
||
| // validate if deleted (column shouldnt exist) | ||
| cy.get(`th:contains(${oldName})`).should("not.exist"); | ||
| cy.get(`th:contains(${newName})`).should("exist"); | ||
| }; | ||
|
|
||
| const addDurationData = (colName, index, cellValue, expectedValue, isNewRow = false) => { | ||
| if (isNewRow) { | ||
| cy.get(".nc-add-new-row-btn:visible").should("exist"); | ||
| cy.wait(500) | ||
| cy.get(".nc-add-new-row-btn").click({ force: true }); | ||
| } else { | ||
| mainPage.getRow(index).find(".nc-row-expand-icon").click({ force: true }); | ||
| } | ||
| cy.get(".duration-cell-wrapper > input").first().should('exist').type(cellValue); | ||
| cy.getActiveModal().find("button").contains("Save row").click({ force: true }); | ||
| cy.toastWait("Row updated successfully"); | ||
| mainPage.getCell(colName, index).find('input').then(($e) => { | ||
| expect($e[0].value).to.equal(expectedValue) | ||
| }) | ||
| } | ||
|
|
||
| /////////////////////////////////////////////////// | ||
| // Test case | ||
| { | ||
| // Duration: h:mm | ||
| it("Duration: h:mm", () => { | ||
| addDurationColumn("NC_DURATION_0", "h:mm (e.g. 1:23)"); | ||
| addDurationData("NC_DURATION_0", 1, "1:30", "01:30", true); | ||
| addDurationData("NC_DURATION_0", 2, "30", "00:30", true); | ||
| addDurationData("NC_DURATION_0", 3, "60", "01:00", true); | ||
| addDurationData("NC_DURATION_0", 4, "80", "01:20", true); | ||
| addDurationData("NC_DURATION_0", 5, "12:34", "12:34", true); | ||
| addDurationData("NC_DURATION_0", 6, "15:130", "17:10", true); | ||
| addDurationData("NC_DURATION_0", 7, "123123", "2052:03", true); | ||
| }); | ||
|
|
||
| it("Duration: Edit Column NC_DURATION_0", () => { | ||
| editColumnByName( | ||
| "NC_DURATION_0", | ||
| "NC_DURATION_EDITED_0", | ||
| "h:mm:ss (e.g. 3:45, 1:23:40)" | ||
| ); | ||
| }); | ||
|
|
||
| it("Duration: Delete column", () => { | ||
| deleteColumnByName("NC_DURATION_EDITED_0"); | ||
| }); | ||
| } | ||
|
|
||
| { | ||
| // Duration: h:mm:ss | ||
| it("Duration: h:mm:ss", () => { | ||
| addDurationColumn("NC_DURATION_1", "h:mm:ss (e.g. 3:45, 1:23:40)"); | ||
| addDurationData("NC_DURATION_1", 1, "11:22:33", "11:22:33"); | ||
| addDurationData("NC_DURATION_1", 2, "1234", "00:20:34"); | ||
| addDurationData("NC_DURATION_1", 3, "50", "00:00:50"); | ||
| addDurationData("NC_DURATION_1", 4, "1:1111", "00:19:31"); | ||
| addDurationData("NC_DURATION_1", 5, "1:11:1111", "01:29:31"); | ||
| addDurationData("NC_DURATION_1", 6, "15:130", "00:17:10"); | ||
| addDurationData("NC_DURATION_1", 7, "123123", "34:12:03"); | ||
| }); | ||
|
|
||
| it("Duration: Edit Column NC_DURATION_1", () => { | ||
| editColumnByName( | ||
| "NC_DURATION_1", | ||
| "NC_DURATION_EDITED_1", | ||
| "h:mm:ss.s (e.g. 3:34.6, 1:23:40.0)" | ||
| ); | ||
| }); | ||
|
|
||
| it("Duration: Delete column", () => { | ||
| deleteColumnByName("NC_DURATION_EDITED_1"); | ||
| }); | ||
| } | ||
|
|
||
| { | ||
| // h:mm:ss.s | ||
| it("Duration: h:mm:ss.s", () => { | ||
| addDurationColumn("NC_DURATION_2", "h:mm:ss.s (e.g. 3:34.6, 1:23:40.0)"); | ||
| addDurationData("NC_DURATION_2", 1, "1234", "00:20:34.0"); | ||
| addDurationData("NC_DURATION_2", 2, "12:34", "00:12:34.0"); | ||
| addDurationData("NC_DURATION_2", 3, "12:34:56", "12:34:56.0"); | ||
| addDurationData("NC_DURATION_2", 4, "12:34:999", "12:50:39.0"); | ||
| addDurationData("NC_DURATION_2", 5, "12:999:56", "28:39:56.0"); | ||
| addDurationData("NC_DURATION_2", 6, "12:34:56.12", "12:34:56.1"); | ||
| addDurationData("NC_DURATION_2", 7, "12:34:56.199", "12:34:56.2"); | ||
| }); | ||
|
|
||
| it("Duration: Edit Column NC_DURATION_2", () => { | ||
| editColumnByName( | ||
| "NC_DURATION_2", | ||
| "NC_DURATION_EDITED_2", | ||
| "h:mm:ss (e.g. 3:45, 1:23:40)" | ||
| ); | ||
| }); | ||
|
|
||
| it("Duration: Delete column", () => { | ||
| deleteColumnByName("NC_DURATION_EDITED_2"); | ||
| }); | ||
| } | ||
|
|
||
| { | ||
| // h:mm:ss.ss | ||
| it("Duration: h:mm:ss.ss", () => { | ||
| addDurationColumn("NC_DURATION_3", "h:mm:ss.ss (e.g. 3.45.67, 1:23:40.00)"); | ||
| addDurationData("NC_DURATION_3", 1, "1234", "00:20:34.00"); | ||
| addDurationData("NC_DURATION_3", 2, "12:34", "00:12:34.00"); | ||
| addDurationData("NC_DURATION_3", 3, "12:34:56", "12:34:56.00"); | ||
| addDurationData("NC_DURATION_3", 4, "12:34:999", "12:50:39.00"); | ||
| addDurationData("NC_DURATION_3", 5, "12:999:56", "28:39:56.00"); | ||
| addDurationData("NC_DURATION_3", 6, "12:34:56.12", "12:34:56.12"); | ||
| addDurationData("NC_DURATION_3", 7, "12:34:56.199", "12:34:56.20"); | ||
| }); | ||
|
|
||
| it("Duration: Edit Column NC_DURATION_3", () => { | ||
| editColumnByName( | ||
| "NC_DURATION_3", | ||
| "NC_DURATION_EDITED_3", | ||
| "h:mm:ss.ss (e.g. 3.45.67, 1:23:40.00)" | ||
| ); | ||
| }); | ||
|
|
||
| it("Duration: Delete column", () => { | ||
| deleteColumnByName("NC_DURATION_EDITED_3"); | ||
| }); | ||
| } | ||
|
|
||
| { | ||
| // h:mm:ss.sss | ||
| it("Duration: h:mm:ss.sss", () => { | ||
| addDurationColumn("NC_DURATION_4", "h:mm:ss.sss (e.g. 3.45.678, 1:23:40.000)"); | ||
| addDurationData("NC_DURATION_4", 1, "1234", "00:20:34.000"); | ||
| addDurationData("NC_DURATION_4", 2, "12:34", "00:12:34.000"); | ||
| addDurationData("NC_DURATION_4", 3, "12:34:56", "12:34:56.000"); | ||
| addDurationData("NC_DURATION_4", 4, "12:34:999", "12:50:39.000"); | ||
| addDurationData("NC_DURATION_4", 5, "12:999:56", "28:39:56.000"); | ||
| addDurationData("NC_DURATION_4", 6, "12:34:56.12", "12:34:56.012"); | ||
| addDurationData("NC_DURATION_4", 7, "12:34:56.199", "12:34:56.199"); | ||
| }); | ||
|
|
||
| it("Duration: Edit Column NC_DURATION_4", () => { | ||
| editColumnByName( | ||
| "NC_DURATION_4", | ||
| "NC_DURATION_EDITED_4", | ||
| "h:mm (e.g. 1:23)" | ||
| ); | ||
| }); | ||
|
|
||
| it("Duration: Delete column", () => { | ||
| deleteColumnByName("NC_DURATION_EDITED_4"); | ||
| }); | ||
| } | ||
| }); | ||
| }; | ||
|
|
||
| /** | ||
| * @copyright Copyright (c) 2021, Xgene Cloud Ltd | ||
| * | ||
| * @author Wing-Kam Wong <wingkwong.code@gmail.com> | ||
| * | ||
| * @license GNU AGPL version 3 or any later version | ||
| * | ||
| * This program is free software: you can redistribute it and/or modify | ||
| * it under the terms of the GNU Affero General Public License as | ||
| * published by the Free Software Foundation, either version 3 of the | ||
| * License, or (at your option) any later version. | ||
| * | ||
| * This program is distributed in the hope that it will be useful, | ||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| * GNU Affero General Public License for more details. | ||
| * | ||
| * You should have received a copy of the GNU Affero General Public License | ||
| * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
| * | ||
| */ |