mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-01-25 02:08:24 +00:00
Feat/sql improvements (#536)
* add indexes * update sql editor file * upgrade schema * optimize Applications.retrieveAll * security fixes * update gh workflows
This commit is contained in:
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@@ -6,22 +6,17 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
docker-compose --version
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
- run: npm install
|
||||
- run: npm run jslint
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
docker-compose --version
|
||||
- run: npm test
|
||||
- run: npm run test:encrypt-decrypt
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: prepare tag
|
||||
id: prepare_tag
|
||||
@@ -38,13 +38,13 @@ jobs:
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.db-create
|
||||
|
||||
6
.github/workflows/docker-publish.yml
vendored
6
.github/workflows/docker-publish.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: prepare tag
|
||||
id: prepare_tag
|
||||
@@ -38,13 +38,13 @@ jobs:
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
@@ -707,6 +707,10 @@ CREATE INDEX sip_gateway_idx_hostport ON sip_gateways (ipv4,port);
|
||||
|
||||
CREATE INDEX idx_sip_gateways_inbound_carrier ON sip_gateways (inbound,voip_carrier_sid);
|
||||
|
||||
CREATE INDEX idx_sip_gateways_inbound_lookup ON sip_gateways (inbound,netmask,ipv4);
|
||||
|
||||
CREATE INDEX idx_sip_gateways_inbound_netmask ON sip_gateways (inbound,netmask);
|
||||
|
||||
CREATE INDEX voip_carrier_sid_idx ON sip_gateways (voip_carrier_sid);
|
||||
ALTER TABLE sip_gateways ADD FOREIGN KEY voip_carrier_sid_idxfk_2 (voip_carrier_sid) REFERENCES voip_carriers (voip_carrier_sid);
|
||||
|
||||
|
||||
@@ -2378,7 +2378,7 @@
|
||||
</location>
|
||||
<size>
|
||||
<width>391.00</width>
|
||||
<height>300.00</height>
|
||||
<height>340.00</height>
|
||||
</size>
|
||||
<zorder>7</zorder>
|
||||
<SQLField>
|
||||
@@ -2508,6 +2508,46 @@
|
||||
<indexNamePrefix><![CDATA[sip_gateways]]></indexNamePrefix>
|
||||
<uid><![CDATA[BCE047C6-F70E-42AD-9201-FECF1BAD6BEA]]></uid>
|
||||
</SQLIndex>
|
||||
<SQLIndex>
|
||||
<name><![CDATA[idx_sip_gateways_inbound_lookup]]></name>
|
||||
<fieldName><![CDATA[inbound]]></fieldName>
|
||||
<fieldName><![CDATA[netmask]]></fieldName>
|
||||
<fieldName><![CDATA[ipv4]]></fieldName>
|
||||
<SQLIndexEntry>
|
||||
<name><![CDATA[inbound]]></name>
|
||||
<prefixSize><![CDATA[]]></prefixSize>
|
||||
<fieldUid><![CDATA[CDE029DC-0C7C-400C-85E9-5005C53B7460]]></fieldUid>
|
||||
</SQLIndexEntry>
|
||||
<SQLIndexEntry>
|
||||
<name><![CDATA[netmask]]></name>
|
||||
<prefixSize><![CDATA[]]></prefixSize>
|
||||
<fieldUid><![CDATA[717ACB37-EF84-48DC-94E4-2AAC066C0A33]]></fieldUid>
|
||||
</SQLIndexEntry>
|
||||
<SQLIndexEntry>
|
||||
<name><![CDATA[ipv4]]></name>
|
||||
<prefixSize><![CDATA[]]></prefixSize>
|
||||
<fieldUid><![CDATA[F18DB7D4-F902-4863-870C-CB07032AE17C]]></fieldUid>
|
||||
</SQLIndexEntry>
|
||||
<indexNamePrefix><![CDATA[sip_gateways]]></indexNamePrefix>
|
||||
<uid><![CDATA[83F405A9-2AE5-415C-9B5E-5E9B92A32F57]]></uid>
|
||||
</SQLIndex>
|
||||
<SQLIndex>
|
||||
<name><![CDATA[idx_sip_gateways_inbound_netmask]]></name>
|
||||
<fieldName><![CDATA[inbound]]></fieldName>
|
||||
<fieldName><![CDATA[netmask]]></fieldName>
|
||||
<SQLIndexEntry>
|
||||
<name><![CDATA[inbound]]></name>
|
||||
<prefixSize><![CDATA[]]></prefixSize>
|
||||
<fieldUid><![CDATA[CDE029DC-0C7C-400C-85E9-5005C53B7460]]></fieldUid>
|
||||
</SQLIndexEntry>
|
||||
<SQLIndexEntry>
|
||||
<name><![CDATA[netmask]]></name>
|
||||
<prefixSize><![CDATA[]]></prefixSize>
|
||||
<fieldUid><![CDATA[717ACB37-EF84-48DC-94E4-2AAC066C0A33]]></fieldUid>
|
||||
</SQLIndexEntry>
|
||||
<indexNamePrefix><![CDATA[sip_gateways]]></indexNamePrefix>
|
||||
<uid><![CDATA[8322B9B7-DC3A-4B0D-85A8-2D15E4C51340]]></uid>
|
||||
</SQLIndex>
|
||||
<labelWindowIndex><![CDATA[31]]></labelWindowIndex>
|
||||
<objectComment><![CDATA[A whitelisted sip gateway used for origination/termination]]></objectComment>
|
||||
<ui.treeExpanded><![CDATA[1]]></ui.treeExpanded>
|
||||
@@ -3193,17 +3233,17 @@
|
||||
<overviewPanelHidden><![CDATA[0]]></overviewPanelHidden>
|
||||
<pageBoundariesVisible><![CDATA[0]]></pageBoundariesVisible>
|
||||
<PageGridVisible><![CDATA[0]]></PageGridVisible>
|
||||
<RightSidebarWidth><![CDATA[1235.000000]]></RightSidebarWidth>
|
||||
<RightSidebarWidth><![CDATA[2944.000000]]></RightSidebarWidth>
|
||||
<sidebarIndex><![CDATA[2]]></sidebarIndex>
|
||||
<snapToGrid><![CDATA[0]]></snapToGrid>
|
||||
<SourceSidebarWidth><![CDATA[0.000000]]></SourceSidebarWidth>
|
||||
<SQLEditorFileFormatVersion><![CDATA[4]]></SQLEditorFileFormatVersion>
|
||||
<uid><![CDATA[58C99A00-06C9-478C-A667-C63842E088F3]]></uid>
|
||||
<windowHeight><![CDATA[876.000000]]></windowHeight>
|
||||
<windowLocationX><![CDATA[-1164.000000]]></windowLocationX>
|
||||
<windowLocationY><![CDATA[1161.000000]]></windowLocationY>
|
||||
<windowHeight><![CDATA[965.000000]]></windowHeight>
|
||||
<windowLocationX><![CDATA[-1886.000000]]></windowLocationX>
|
||||
<windowLocationY><![CDATA[1072.000000]]></windowLocationY>
|
||||
<windowScrollOrigin><![CDATA[{0, 0}]]></windowScrollOrigin>
|
||||
<windowWidth><![CDATA[1512.000000]]></windowWidth>
|
||||
<windowWidth><![CDATA[3221.000000]]></windowWidth>
|
||||
</SQLDocumentInfo>
|
||||
<AllowsIndexRenamingOnInsert><![CDATA[1]]></AllowsIndexRenamingOnInsert>
|
||||
<defaultLabelExpanded><![CDATA[1]]></defaultLabelExpanded>
|
||||
|
||||
@@ -235,9 +235,10 @@ const sql = {
|
||||
'ALTER TABLE voip_carriers ADD COLUMN trunk_type ENUM(\'static_ip\',\'auth\',\'reg\') NOT NULL DEFAULT \'static_ip\'',
|
||||
'ALTER TABLE predefined_carriers ADD COLUMN trunk_type ENUM(\'static_ip\',\'auth\',\'reg\') NOT NULL DEFAULT \'static_ip\'',
|
||||
'CREATE INDEX idx_sip_gateways_inbound_carrier ON sip_gateways (inbound,voip_carrier_sid)',
|
||||
]
|
||||
'CREATE INDEX idx_sip_gateways_inbound_lookup ON sip_gateways (inbound,netmask,ipv4)',
|
||||
'CREATE INDEX idx_sip_gateways_inbound_netmask ON sip_gateways (inbound,netmask)'
|
||||
],
|
||||
};
|
||||
|
||||
const doIt = async() => {
|
||||
let connection;
|
||||
try {
|
||||
|
||||
@@ -54,9 +54,19 @@ class Application extends Model {
|
||||
}
|
||||
|
||||
static countAll(obj) {
|
||||
let sql = 'SELECT COUNT(*) AS count FROM applications app WHERE 1 = 1';
|
||||
const args = [];
|
||||
sql += Application._criteriaBuilder(obj, args);
|
||||
const criteriaClause = Application._criteriaBuilder(obj, args);
|
||||
|
||||
// Only use "WHERE 1 = 1" if there are no filters
|
||||
// Otherwise start with the actual filter for better index usage
|
||||
let sql;
|
||||
if (criteriaClause) {
|
||||
// Remove leading ' AND ' from criteriaBuilder output and use as WHERE clause
|
||||
sql = 'SELECT COUNT(*) AS count FROM applications app WHERE ' + criteriaClause.substring(5);
|
||||
} else {
|
||||
// No filters provided - count all applications
|
||||
sql = 'SELECT COUNT(*) AS count FROM applications app WHERE 1 = 1';
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
getMysqlConnection((err, conn) => {
|
||||
if (err) return reject(err);
|
||||
@@ -123,9 +133,19 @@ class Application extends Model {
|
||||
}
|
||||
|
||||
// No pagination - use original query
|
||||
let sql = retrieveSql + ' WHERE 1 = 1';
|
||||
const args = [];
|
||||
sql += Application._criteriaBuilder(obj, args);
|
||||
const criteriaClause = Application._criteriaBuilder(obj, args);
|
||||
|
||||
// Only use "WHERE 1 = 1" if there are no filters
|
||||
// Otherwise start with the actual filter for better index usage
|
||||
let sql;
|
||||
if (criteriaClause) {
|
||||
// Remove leading ' AND ' from criteriaBuilder output and use as WHERE clause
|
||||
sql = retrieveSql + ' WHERE ' + criteriaClause.substring(5);
|
||||
} else {
|
||||
// No filters provided - must list all applications
|
||||
sql = retrieveSql + ' WHERE 1 = 1';
|
||||
}
|
||||
sql += ' ORDER BY app.application_sid';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
3978
package-lock.json
generated
3978
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -285,3 +285,177 @@ test('application tests', async(t) => {
|
||||
}
|
||||
});
|
||||
|
||||
test('application query optimization tests', async(t) => {
|
||||
const app = require('../app');
|
||||
try {
|
||||
let result;
|
||||
|
||||
/* Create multiple service providers and accounts to test filtering */
|
||||
const voip_carrier_sid = await createVoipCarrier(request, 'test-carrier-apps');
|
||||
|
||||
const service_provider_sid_1 = await createServiceProvider(request, 'test-sp-1');
|
||||
const service_provider_sid_2 = await createServiceProvider(request, 'test-sp-2');
|
||||
|
||||
const account_sid_1 = await createAccount(request, service_provider_sid_1, 'test-account-1');
|
||||
const account_sid_2 = await createAccount(request, service_provider_sid_2, 'test-account-2');
|
||||
|
||||
/* Create applications for different accounts */
|
||||
const app1_result = await request.post('/Applications', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'test-app-account-1',
|
||||
account_sid: account_sid_1,
|
||||
call_hook: { url: 'http://example.com/app1' },
|
||||
call_status_hook: { url: 'http://example.com/app1/status' }
|
||||
}
|
||||
});
|
||||
const app1_sid = app1_result.sid;
|
||||
|
||||
const app2_result = await request.post('/Applications', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'test-app-account-2',
|
||||
account_sid: account_sid_2,
|
||||
call_hook: { url: 'http://example.com/app2' },
|
||||
call_status_hook: { url: 'http://example.com/app2/status' }
|
||||
}
|
||||
});
|
||||
const app2_sid = app2_result.sid;
|
||||
|
||||
const app3_result = await request.post('/Applications', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
name: 'another-app-account-1',
|
||||
account_sid: account_sid_1,
|
||||
call_hook: { url: 'http://example.com/app3' },
|
||||
call_status_hook: { url: 'http://example.com/app3/status' }
|
||||
}
|
||||
});
|
||||
const app3_sid = app3_result.sid;
|
||||
|
||||
/* Test 1: Query all applications as admin (no filter) - tests WHERE 1=1 fallback */
|
||||
result = await request.get('/Applications', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length >= 3, 'admin can see all applications using WHERE 1=1');
|
||||
const ourApps = result.filter(app =>
|
||||
[app1_sid, app2_sid, app3_sid].includes(app.application_sid)
|
||||
);
|
||||
t.ok(ourApps.length === 3, 'all three test applications are included in results');
|
||||
|
||||
/* Test 2: Query applications with name filter (LIKE query) - tests WHERE name LIKE optimization */
|
||||
result = await request.get('/Applications', {
|
||||
qs: { name: 'test-app' },
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 2, 'successfully filtered applications by name prefix');
|
||||
t.ok(result.every(app => app.name.includes('test-app')), 'all results match name filter');
|
||||
|
||||
/* Test 3: Query applications with exact name match - tests WHERE optimization */
|
||||
result = await request.get('/Applications', {
|
||||
qs: { name: 'test-app-account-1' },
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 1, 'successfully filtered applications by exact name');
|
||||
t.ok(result[0].name === 'test-app-account-1', 'exact name match works correctly');
|
||||
|
||||
/* Test 4: Query with name filter that matches nothing */
|
||||
result = await request.get('/Applications', {
|
||||
qs: { name: 'nonexistent-app-12345' },
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 0, 'non-matching name filter returns empty array');
|
||||
|
||||
/* Test 5: Query with pagination and name filter - tests countAll optimization */
|
||||
result = await request.get('/Applications', {
|
||||
qs: {
|
||||
name: 'test-app',
|
||||
page: 1,
|
||||
page_size: 10
|
||||
},
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 2, 'pagination with name filter returns correct count');
|
||||
t.ok(result.total === 2, 'countAll with name filter returns correct total');
|
||||
t.ok(result.page === 1, 'pagination returns correct page number');
|
||||
t.ok(result.page_size === 10, 'pagination returns correct page size');
|
||||
|
||||
/* Test 6: Query with pagination and no filter - tests WHERE 1=1 fallback */
|
||||
result = await request.get('/Applications', {
|
||||
qs: {
|
||||
page: 1,
|
||||
page_size: 2
|
||||
},
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 2, 'pagination without filter returns page_size results');
|
||||
t.ok(result.total >= 3, 'pagination without filter uses WHERE 1=1 and returns all');
|
||||
|
||||
/* Test 7: Create SP-scoped token and verify WHERE service_provider_sid optimization */
|
||||
const sp1_token_result = await request.post('/ApiKeys', {
|
||||
auth: authAdmin,
|
||||
json: true,
|
||||
body: {
|
||||
service_provider_sid: service_provider_sid_1
|
||||
}
|
||||
});
|
||||
const sp1_token = sp1_token_result.token;
|
||||
const sp1_token_sid = sp1_token_result.sid;
|
||||
|
||||
result = await request.get('/Applications', {
|
||||
auth: {bearer: sp1_token},
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 2, 'SP-scoped token sees only their applications via WHERE service_provider_sid');
|
||||
t.ok(result.every(app => app.account_sid === account_sid_1), 'all apps belong to SP1 accounts');
|
||||
|
||||
/* Test 8: SP-scoped token with name filter - tests combined WHERE clause */
|
||||
result = await request.get('/Applications', {
|
||||
qs: { name: 'test-app' },
|
||||
auth: {bearer: sp1_token},
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.length === 1, 'SP-scoped token with name filter combines filters correctly');
|
||||
t.ok(result[0].name === 'test-app-account-1', 'combined filter returns correct app');
|
||||
|
||||
/* Test 9: SP-scoped token with pagination - tests countAll with service_provider_sid */
|
||||
result = await request.get('/Applications', {
|
||||
qs: {
|
||||
page: 1,
|
||||
page_size: 10
|
||||
},
|
||||
auth: {bearer: sp1_token},
|
||||
json: true,
|
||||
});
|
||||
t.ok(result.data.length === 2, 'SP-scoped pagination returns correct count');
|
||||
t.ok(result.total === 2, 'countAll with service_provider_sid returns correct total');
|
||||
|
||||
/* Cleanup tokens */
|
||||
await deleteObjectBySid(request, '/ApiKeys', sp1_token_sid);
|
||||
|
||||
/* Cleanup */
|
||||
await deleteObjectBySid(request, '/Applications', app1_sid);
|
||||
await deleteObjectBySid(request, '/Applications', app2_sid);
|
||||
await deleteObjectBySid(request, '/Applications', app3_sid);
|
||||
await deleteObjectBySid(request, '/Accounts', account_sid_1);
|
||||
await deleteObjectBySid(request, '/Accounts', account_sid_2);
|
||||
await deleteObjectBySid(request, '/VoipCarriers', voip_carrier_sid);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid_1);
|
||||
await deleteObjectBySid(request, '/ServiceProviders', service_provider_sid_2);
|
||||
|
||||
//t.end();
|
||||
}
|
||||
catch (err) {
|
||||
t.end(err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user