Browse Source

feat: socket apis

Signed-off-by: mertmit <mertmit99@gmail.com>
feat/socket-api
mertmit 2 years ago
parent
commit
cdc5bd32be
  1. 15
      packages/nc-gui/composables/useApi/index.ts
  2. 29
      packages/nc-gui/composables/useApi/interceptors.ts
  3. 78
      packages/nc-gui/composables/useSocketApi.ts
  4. 96
      packages/nc-gui/package-lock.json
  5. 2
      packages/nc-gui/package.json
  6. 4
      packages/nc-gui/pages/signin.vue
  7. 210
      packages/nocodb/src/lib/meta/api/index.ts

15
packages/nc-gui/composables/useApi/index.ts

@ -58,6 +58,8 @@ export function useApi<Data = any, RequestConfig = any>({
const nuxtApp = useNuxtApp()
useSocketApi()
/** api instance - with interceptors for token refresh already bound */
const api = useGlobalInstance && !!nuxtApp.$api ? nuxtApp.$api : createApiInstance(apiOptions)
@ -138,6 +140,19 @@ export function useApi<Data = any, RequestConfig = any>({
},
)
api.instance.interceptors.response.use(
(response) => {
return response
},
(rs) => {
if (rs?.socket) {
isLoading.value = false
return Promise.resolve(rs)
}
return rs
},
)
return {
api,
isLoading,

29
packages/nc-gui/composables/useApi/interceptors.ts

@ -7,6 +7,35 @@ export function addAxiosInterceptors(api: Api<any>) {
const state = useGlobal()
const router = useRouter()
const route = $(router.currentRoute)
const { socket, request } = useSocketApi()
// first interceptor is executed last
api.instance.interceptors.request.use(
async (config) => {
if (socket.value?.connected) {
await request({
...config,
headers: {
...config.headers,
host: window.location.host,
},
protocol: window.location.protocol,
})
.then((t: any) => {
throw t
})
.catch((e: any) => {
if (e?.socket) throw e
})
}
return config
},
(rs) => {
if (rs?.socket) return Promise.resolve(rs)
return rs
},
)
api.instance.interceptors.request.use((config) => {
config.headers['xc-gui'] = 'true'

78
packages/nc-gui/composables/useSocketApi.ts

@ -0,0 +1,78 @@
import type { Socket } from 'socket.io-client'
import io from 'socket.io-client'
import type { Ref } from 'vue'
import { v4 as uuidv4 } from 'uuid'
import { useGlobal } from '#imports'
export const useSocketApi = createSharedComposable(() => {
const state = useGlobal()
const requestQuery: Record<string, any> = {}
const socket: Ref<Socket | undefined> = ref()
const init = async (token: string) => {
try {
if (socket.value) socket.value.disconnect()
const url = new URL(state.appInfo.value.ncSiteUrl, window.location.href.split(/[?#]/)[0]).href
socket.value = io(url, {
extraHeaders: { 'xc-auth': token },
})
socket.value.on('connect_error', () => {
socket.value?.disconnect()
})
socket.value.on('api', (data) => {
if (data.id in requestQuery) {
const tq = requestQuery[data.id]
if (data?.setCookie) {
for (const ck of data.setCookie) {
// TODO fix event
setCookie({} as any, ck.name, ck.value, ck.options)
}
}
tq.promise.resolve(data)
delete requestQuery[data.id]
} else {
console.log('Unknown query', data)
}
})
} catch {}
}
watch(
state.token,
(newToken, oldToken) => {
if (newToken && newToken !== oldToken) init(newToken)
else if (!newToken) socket.value?.disconnect()
},
{ immediate: true },
)
return {
init,
socket,
request: (data: any): Promise<any> => {
return new Promise((resolve, reject) => {
if (!socket.value) return reject(new Error('Socket not initialized'))
const tq = {
id: uuidv4(),
promise: {
resolve,
reject,
},
data,
dt: Date.now(),
}
requestQuery[tq.id] = tq
socket.value?.emit('api', { id: tq.id, ...tq.data })
})
},
}
})

96
packages/nc-gui/package-lock.json generated

@ -36,6 +36,7 @@
"sortablejs": "^1.15.0",
"tinycolor2": "^1.4.2",
"unique-names-generator": "^4.7.1",
"uuid": "^9.0.0",
"v3-infinite-loading": "^1.2.2",
"validator": "^13.7.0",
"vue-dompurify-html": "^3.0.0",
@ -71,6 +72,7 @@
"@types/papaparse": "^5.3.2",
"@types/sortablejs": "^1.13.0",
"@types/tinycolor2": "^1.4.3",
"@types/uuid": "^9.0.0",
"@types/validator": "^13.7.10",
"@vitest/ui": "^0.18.0",
"@vue/compiler-sfc": "^3.2.37",
@ -95,28 +97,6 @@
"windicss": "^3.5.6"
}
},
"../nocodb-sdk": {
"version": "0.104.3",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"cspell": "^4.1.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"typescript": "^4.0.2"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
@ -3204,6 +3184,12 @@
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==",
"dev": true
},
"node_modules/@types/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==",
"dev": true
},
"node_modules/@types/validator": {
"version": "13.7.10",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.10.tgz",
@ -8544,7 +8530,6 @@
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"devOptional": true,
"funding": [
{
"type": "individual",
@ -11960,8 +11945,21 @@
}
},
"node_modules/nocodb-sdk": {
"resolved": "../nocodb-sdk",
"link": true
"version": "0.104.3",
"resolved": "file:../nocodb-sdk",
"license": "AGPL-3.0-or-later",
"dependencies": {
"axios": "^0.21.1",
"jsep": "^1.3.6"
}
},
"node_modules/nocodb-sdk/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/node-abi": {
"version": "3.23.0",
@ -16258,6 +16256,14 @@
"node": ">= 0.4.0"
}
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/v3-infinite-loading": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/v3-infinite-loading/-/v3-infinite-loading-1.2.2.tgz",
@ -20125,6 +20131,12 @@
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==",
"dev": true
},
"@types/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q==",
"dev": true
},
"@types/validator": {
"version": "13.7.10",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.10.tgz",
@ -23943,8 +23955,7 @@
"follow-redirects": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==",
"devOptional": true
"integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA=="
},
"form-data": {
"version": "4.0.0",
@ -26417,22 +26428,20 @@
}
},
"nocodb-sdk": {
"version": "file:../nocodb-sdk",
"version": "0.104.3",
"requires": {
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"axios": "^0.21.1",
"cspell": "^4.1.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-prettier": "^4.0.0",
"jsep": "^1.3.6",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"typescript": "^4.0.2"
"jsep": "^1.3.6"
},
"dependencies": {
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
}
}
},
"node-abi": {
@ -29634,6 +29643,11 @@
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
"dev": true
},
"uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
},
"v3-infinite-loading": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/v3-infinite-loading/-/v3-infinite-loading-1.2.2.tgz",

2
packages/nc-gui/package.json

@ -59,6 +59,7 @@
"sortablejs": "^1.15.0",
"tinycolor2": "^1.4.2",
"unique-names-generator": "^4.7.1",
"uuid": "^9.0.0",
"v3-infinite-loading": "^1.2.2",
"validator": "^13.7.0",
"vue-dompurify-html": "^3.0.0",
@ -94,6 +95,7 @@
"@types/papaparse": "^5.3.2",
"@types/sortablejs": "^1.13.0",
"@types/tinycolor2": "^1.4.3",
"@types/uuid": "^9.0.0",
"@types/validator": "^13.7.10",
"@vitest/ui": "^0.18.0",
"@vue/compiler-sfc": "^3.2.37",

4
packages/nc-gui/pages/signin.vue

@ -13,6 +13,8 @@ const { api, isLoading, error } = useApi({ useGlobalInstance: true })
const { t } = useI18n()
const { init } = useSocketApi()
useSidebar('nc-left-sidebar', { hasSidebar: false })
const formValidator = ref()
@ -52,6 +54,8 @@ async function signIn() {
api.auth.signin(form).then(async ({ token }) => {
_signIn(token!)
await init(token!)
await navigateTo('/')
})
}

210
packages/nocodb/src/lib/meta/api/index.ts

@ -58,6 +58,9 @@ import syncSourceApis from './sync/syncSourceApis';
const clients: { [id: string]: Socket } = {};
const jobs: { [id: string]: { last_message: any } } = {};
const middlewares = [];
const socketApis = [];
export default function (router: Router, server) {
initStrategies(router);
projectApis(router);
@ -148,9 +151,216 @@ export default function (router: Router, server) {
socket.emit('progress', jobs[room].last_message);
}
});
socket.on('api', (data) => {
const startAt = process.hrtime();
const params = {};
const hf = socketApis.find((hf) => {
if (hf.method !== data.method) return false;
const hfpath = hf.path.replace(/\/$/, '').split('/')
const dtpath = data.url.replace(/\?.+/, '').replace(/\/$/, '').split('/')
if (hfpath.length !== dtpath.length) return false;
for (let i = 0; i < hfpath.length; i++) {
if (/^:/.test(hfpath[i])) {
if (!dtpath[i]) return false;
params[hfpath[i].substr(1)] = dtpath[i];
} else {
if (hfpath[i] !== dtpath[i]) return false;
}
}
return true;
});
if (hf) {
const headers = Object.entries(data.headers)
.reduce((obj, [key, val]) => {
if (typeof val === 'string') {
obj[key] = val;
} else if (typeof val === 'object') {
if (key === 'common' || key === data.method) {
obj = { ...obj, ...val };
}
}
return obj;
}, { 'xc-socket': 'true'});
data.path = hf.path;
data.query = data.params || {};
data.params = params;
data.headers = headers;
data.body = data.data;
data.originalUrl = data.url;
const req = createReq(data);
const res = createRes((_cd, dt, _hd) => {
const rt: any = { id: data.id, socket: true, data: dt }
if (dt?.setCookie) {
rt.setCookie = dt.setCookie;
delete dt.setCookie;
}
const ms = process.hrtime(startAt)[0] * 1e3 + process.hrtime(startAt)[1] * 1e-6;
// :method :url :status - :response-time ms
process.stdout.write(`${req.method } ${req.url} ${res.statusCode} - ${ms.toFixed(3)} ms\n`)
socket.emit('api', rt);
})
for (const middleware of middlewares) {
middleware.handle(req, res, () => {});
}
hf.handle(req, res, () => {})
}
});
});
importApis(router, io, jobs);
socketApis.push(...handleRouter(router));
}
function createReq(data: any) {
const req: any = { ...data };
req.header = req.get = (p) => req.headers[p];
req.login =
req.logIn = function(user, options, done) {
if (typeof options == 'function') {
done = options;
options = {};
}
options = options || {};
var property = 'user';
if (this._passport && this._passport.instance) {
property = this._passport.instance._userProperty || 'user';
}
var session = (options.session === undefined) ? true : options.session;
this[property] = user;
if (session) {
if (!this._passport) { throw new Error('passport.initialize() middleware not in use'); }
if (typeof done != 'function') { throw new Error('req#login requires a callback function'); }
var self = this;
this._passport.instance._sm.logIn(this, user, function(err) {
if (err) { self[property] = null; return done(err); }
done();
});
} else {
done && done();
}
};
req.logout =
req.logOut = function() {
var property = 'user';
if (this._passport && this._passport.instance) {
property = this._passport.instance._userProperty || 'user';
}
this[property] = null;
if (this._passport) {
this._passport.instance._sm.logOut(this);
}
};
req.isAuthenticated = function() {
var property = 'user';
if (this._passport && this._passport.instance) {
property = this._passport.instance._userProperty || 'user';
}
return (this[property]) ? true : false;
};
req.isUnauthenticated = function() {
return !this.isAuthenticated();
};
return req;
}
function createRes(callback) {
var res: any = {
_removedHeader: {},
headersSent: true,
statusMessage: 'OK',
statusCode: 200,
locals: {},
};
var headers = {};
res.set = res.header = (x, y) => {
if (arguments.length === 2) {
res.setHeader(x, y);
} else {
for (var key in x) {
res.setHeader(key, x[key]);
}
}
return res;
}
res.setHeader = (x, y) => {
headers[x] = y;
headers[x.toLowerCase()] = y;
return res;
};
res.getHeader = (x) => headers[x];
res.redirect = function(_code, url) {
if (Number.isNaN(_code)) {
res.statusCode = 301;
url = _code;
} else {
res.statusCode = _code;
}
res.setHeader("Location", url);
res.end();
};
res.status = res.sendStatus = function(number) {
res.statusCode = number;
return res;
};
res.cookie = function(name, value, options) {
if (callback) callback(res.statusCode, { setCookie: [ { name, value, options} ] }, {})
}
res.end = res.send = res.write = res.json = function(data) {
if (callback) callback(res.statusCode, data, headers);
};
return res;
}
function handleRouter(router: any) {
if (!router.stack) return [];
const tempRoutes = [];
for (const layer of router.stack) {
if (layer.name !== 'router' && layer.name !== 'logger' && layer.route === undefined && typeof layer.handle === 'function') {
const rt = {
method: 'MIDDLEWARE',
handle: layer.handle,
}
middlewares.push(rt);
}
if (layer.name === 'bound dispatch' && typeof layer.route.path === 'string') {
const rt = {
method: Object.keys(layer.route.methods)[0],
path: layer.route.path,
stack: layer.route.stack,
handle: layer.handle,
}
tempRoutes.push(rt);
} else if (layer.name === 'router') {
tempRoutes.push(...handleRouter(layer.handle));
}
}
return tempRoutes;
}
function getHash(str) {

Loading…
Cancel
Save