notu 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/notu.js +2060 -0
- package/dist/notu.umd.cjs +5 -0
- package/package.json +6 -1
- package/src/Environment.test.ts +76 -0
- package/src/Environment.ts +51 -0
- package/src/index.ts +17 -1
- package/src/models/Attr.test.ts +21 -0
- package/src/models/Attr.ts +14 -0
- package/src/models/Note.test.ts +85 -0
- package/src/models/Note.ts +40 -0
- package/src/models/NoteAttr.test.ts +358 -0
- package/src/models/NoteAttr.ts +113 -0
- package/src/models/NoteTag.test.ts +45 -0
- package/src/models/NoteTag.ts +15 -0
- package/src/models/Tag.test.ts +62 -0
- package/src/models/Tag.ts +15 -0
- package/src/services/HttpClient.test.ts +108 -0
- package/src/services/HttpClient.ts +151 -0
- package/src/services/QueryParser.test.ts +153 -0
- package/src/services/QueryParser.ts +137 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import HttpClient from './HttpClient';
|
|
3
|
+
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
|
|
4
|
+
import Space from '../models/Space';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
function mockAxiosResponse(status: number, data: any): AxiosResponse<any, any> {
|
|
8
|
+
const response = {
|
|
9
|
+
data,
|
|
10
|
+
status,
|
|
11
|
+
statusText: 'hello',
|
|
12
|
+
headers: null,
|
|
13
|
+
config: null
|
|
14
|
+
};
|
|
15
|
+
if (status < 200 || status >= 300) {
|
|
16
|
+
const error = new Error('Something went wrong');
|
|
17
|
+
error['response'] = response;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
return response;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function mockAxios(config: AxiosRequestConfig<any>): Promise<AxiosResponse<any, any>> {
|
|
24
|
+
if (config.method == 'post' && config.url.endsWith('/login')) {
|
|
25
|
+
if (config.data.username == 'ValidUser' && config.data.password == 'ValidPassword')
|
|
26
|
+
return mockAxiosResponse(200, 'qwer.asdf.zxcv');
|
|
27
|
+
return mockAxiosResponse(401, null);
|
|
28
|
+
}
|
|
29
|
+
if (config.method == 'get' && config.url.endsWith('/spaces')) {
|
|
30
|
+
return mockAxiosResponse(200, [
|
|
31
|
+
new Space('Space 1'),
|
|
32
|
+
new Space('Space 2')
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
return mockAxiosResponse(404, null);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
test('constructor takes api root url', () => {
|
|
42
|
+
const client = new HttpClient('https://www.test.com');
|
|
43
|
+
|
|
44
|
+
expect(client.url).toBe('https://www.test.com');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('constructor throws error if url not provided', () => {
|
|
48
|
+
expect(() => new HttpClient(null)).toThrowError();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('If no httpRequester method passed in, then defaults to using axios', () => {
|
|
52
|
+
const client = new HttpClient('abcd');
|
|
53
|
+
|
|
54
|
+
expect(client['_httpRequester']).toBe(axios);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
test('login asyncronously gets token from web service', async () => {
|
|
59
|
+
const client = new HttpClient('abcd', mockAxios);
|
|
60
|
+
const loginResult = await client.login('ValidUser', 'ValidPassword');
|
|
61
|
+
|
|
62
|
+
expect(loginResult.success).toBe(true);
|
|
63
|
+
expect(loginResult.error).toBeNull();
|
|
64
|
+
expect(loginResult.token).toBe('qwer.asdf.zxcv');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('login fails if username and password are invalid', async () => {
|
|
68
|
+
const client = new HttpClient('abcd', mockAxios);
|
|
69
|
+
const loginResult = await client.login('InvalidUser', 'InvalidPassword');
|
|
70
|
+
|
|
71
|
+
expect(loginResult.success).toBe(false);
|
|
72
|
+
expect(loginResult.error).toBe('Invalid username & password.');
|
|
73
|
+
expect(loginResult.token).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('login sets token on the client', async () => {
|
|
77
|
+
const client = new HttpClient('abcd', mockAxios);
|
|
78
|
+
await client.login('ValidUser', 'ValidPassword');
|
|
79
|
+
|
|
80
|
+
expect(client.token).toBe('qwer.asdf.zxcv');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('Token can be manually set if already known', () => {
|
|
84
|
+
const client = new HttpClient('abcd', mockAxios);
|
|
85
|
+
client.token = 'qwer.asdf.zxcv';
|
|
86
|
+
|
|
87
|
+
expect(client.token).toBe('qwer.asdf.zxcv');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
test('getSpaces makes async call to correct URL endpoint, returns space objects', async () => {
|
|
92
|
+
let request: AxiosRequestConfig<any> = null;
|
|
93
|
+
const client = new HttpClient('abcd', r => {
|
|
94
|
+
request = r;
|
|
95
|
+
return mockAxios(r);
|
|
96
|
+
});
|
|
97
|
+
client.token = 'qwer.asdf.zxcv';
|
|
98
|
+
|
|
99
|
+
const result = await client.getSpaces();
|
|
100
|
+
|
|
101
|
+
expect(request.method).toBe('get');
|
|
102
|
+
expect(request.url).toBe('abcd/spaces');
|
|
103
|
+
expect(request.data).toBeFalsy();
|
|
104
|
+
expect(request.headers.Authorization).toBe('Bearer qwer.asdf.zxcv');
|
|
105
|
+
expect(result.length).toBe(2);
|
|
106
|
+
expect(result[0].name).toBe('Space 1');
|
|
107
|
+
expect(result[1].name).toBe('Space 2');
|
|
108
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
|
|
4
|
+
import Space from "../models/Space";
|
|
5
|
+
import { Note } from "..";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export interface NotuLoginResult {
|
|
9
|
+
success: boolean;
|
|
10
|
+
error: string;
|
|
11
|
+
token: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export interface NotuClient {
|
|
16
|
+
login(username: string, password: string): Promise<NotuLoginResult>;
|
|
17
|
+
|
|
18
|
+
getSpaces(): Promise<Array<Space>>;
|
|
19
|
+
|
|
20
|
+
saveSpace(space: Space): Promise<Space>;
|
|
21
|
+
|
|
22
|
+
getNotes(query: string, spaceId: number): Promise<Array<Note>>;
|
|
23
|
+
|
|
24
|
+
getNoteCount(query: string, spaceId: number): Promise<number>;
|
|
25
|
+
|
|
26
|
+
saveNotes(notes: Array<Note>): Promise<Array<Note>>;
|
|
27
|
+
|
|
28
|
+
customJob(name: string, data: any): Promise<any>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
export default class HttpClient {
|
|
33
|
+
|
|
34
|
+
private _url: string = null;
|
|
35
|
+
get url(): string { return this._url; }
|
|
36
|
+
|
|
37
|
+
private _token: string = null;
|
|
38
|
+
get token(): string { return this._token; }
|
|
39
|
+
set token(value: string) { this._token = value; }
|
|
40
|
+
|
|
41
|
+
//Added for testing support
|
|
42
|
+
private _httpRequester: (config: AxiosRequestConfig<any>) => Promise<AxiosResponse<any, any>>;
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
url: string,
|
|
46
|
+
httpRequester: (config: AxiosRequestConfig<any>) => Promise<AxiosResponse<any, any>> = null
|
|
47
|
+
) {
|
|
48
|
+
if (!url)
|
|
49
|
+
throw Error('Endpoint URL must be passed in to NotuClient constructor');
|
|
50
|
+
this._url = url;
|
|
51
|
+
this._httpRequester = httpRequester ?? axios;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async login(username: string, password: string): Promise<NotuLoginResult> {
|
|
56
|
+
try {
|
|
57
|
+
const result = await this._httpRequester({
|
|
58
|
+
method: 'post',
|
|
59
|
+
url: (this.url + '/login').replace('//', '/'),
|
|
60
|
+
data: { username, password }
|
|
61
|
+
});
|
|
62
|
+
this._token = result.data;
|
|
63
|
+
return { success: true, error: null, token: result.data };
|
|
64
|
+
}
|
|
65
|
+
catch (ex) {
|
|
66
|
+
if (ex.response.status == 401)
|
|
67
|
+
return { success: false, error: 'Invalid username & password.', token: null };
|
|
68
|
+
throw ex;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async getSpaces(): Promise<Array<Space>> {
|
|
74
|
+
const result = await this._httpRequester({
|
|
75
|
+
method: 'get',
|
|
76
|
+
url: (this.url + '/spaces').replace('//', '/'),
|
|
77
|
+
headers: {
|
|
78
|
+
Authorization: 'Bearer ' + this.token
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return result.data;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async saveSpace(space: Space): Promise<Space> {
|
|
86
|
+
const result = await this._httpRequester({
|
|
87
|
+
method: 'post',
|
|
88
|
+
url: (this.url + '/spaces').replace('//', '/'),
|
|
89
|
+
data: space,
|
|
90
|
+
headers: {
|
|
91
|
+
Authorization: 'Bearer ' + this.token
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return result.data;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async getNotes(query: string, spaceId: number): Promise<Array<Note>> {
|
|
100
|
+
const result = await this._httpRequester({
|
|
101
|
+
method: 'get',
|
|
102
|
+
url: (this.url + '/notes').replace('//', '/'),
|
|
103
|
+
data: { query, spaceId },
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: 'Bearer ' + this.token
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return result.data;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async getNoteCount(query: string, spaceId: number): Promise<number> {
|
|
113
|
+
const result = await this._httpRequester({
|
|
114
|
+
method: 'get',
|
|
115
|
+
url: (this.url + '/notes').replace('//', '/'),
|
|
116
|
+
data: { query, spaceId },
|
|
117
|
+
headers: {
|
|
118
|
+
Authorization: 'Bearer ' + this.token
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return result.data;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async saveNotes(notes: Array<Note>): Promise<Array<Note>> {
|
|
126
|
+
const result = await this._httpRequester({
|
|
127
|
+
method: 'post',
|
|
128
|
+
url: (this.url + '/notes').replace('//', '/'),
|
|
129
|
+
data: { notes },
|
|
130
|
+
headers: {
|
|
131
|
+
Authorization: 'Bearer ' + this.token
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return result.data;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async customJob(name: string, data: any): Promise<any> {
|
|
140
|
+
const result = await this._httpRequester({
|
|
141
|
+
method: 'post',
|
|
142
|
+
url: (this.url + 'customjob').replace('//', '/'),
|
|
143
|
+
data: { name, data },
|
|
144
|
+
headers: {
|
|
145
|
+
Authorization: 'Bearer ' + this.token
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return result.data;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import { splitQuery, identifyTags, identifyAttrs } from './QueryParser';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
test('splitQuery should split out string into where, order', () => {
|
|
6
|
+
const result = splitQuery('#Test AND #Info ORDER BY @Price DESC');
|
|
7
|
+
|
|
8
|
+
expect(result.where).toBe('#Test AND #Info');
|
|
9
|
+
expect(result.order).toBe('@Price DESC');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('splitQuery should leave order null if not specified', () => {
|
|
13
|
+
const result = splitQuery('#Test AND #Info');
|
|
14
|
+
|
|
15
|
+
expect(result.where).toBe('#Test AND #Info');
|
|
16
|
+
expect(result.order).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('splitQuery should leave where null if not specified', () => {
|
|
20
|
+
const result = splitQuery('ORDER BY @Price DESC');
|
|
21
|
+
|
|
22
|
+
expect(result.where).toBeNull();
|
|
23
|
+
expect(result.order).toBe('@Price DESC');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
test('identifyTags should correctly identify multiple tags in query', () => {
|
|
28
|
+
const result = splitQuery('#Test AND ~Info OR #~Me');
|
|
29
|
+
|
|
30
|
+
result.where = identifyTags(result.where, result);
|
|
31
|
+
|
|
32
|
+
expect(result.where).toBe('{tag0} AND {tag1} OR {tag2}');
|
|
33
|
+
expect(result.tags[0].space).toBeNull();
|
|
34
|
+
expect(result.tags[0].name).toBe('Test');
|
|
35
|
+
expect(result.tags[0].includeOwner).toBe(false);
|
|
36
|
+
expect(result.tags[0].searchDepth).toBe(1);
|
|
37
|
+
expect(result.tags[0].strictSearchDepth).toBe(true);
|
|
38
|
+
expect(result.tags[1].space).toBeNull();
|
|
39
|
+
expect(result.tags[1].name).toBe('Info');
|
|
40
|
+
expect(result.tags[1].includeOwner).toBe(true);
|
|
41
|
+
expect(result.tags[1].searchDepth).toBe(0);
|
|
42
|
+
expect(result.tags[1].strictSearchDepth).toBe(true);
|
|
43
|
+
expect(result.tags[2].space).toBeNull();
|
|
44
|
+
expect(result.tags[2].name).toBe('Me');
|
|
45
|
+
expect(result.tags[2].includeOwner).toBe(true);
|
|
46
|
+
expect(result.tags[2].searchDepth).toBe(1);
|
|
47
|
+
expect(result.tags[2].strictSearchDepth).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('identifyTags handles spaces in tag names if wrapped in brackets', () => {
|
|
51
|
+
const result = splitQuery('#[I Am Long]');
|
|
52
|
+
|
|
53
|
+
result.where = identifyTags(result.where, result);
|
|
54
|
+
|
|
55
|
+
expect(result.where).toBe('{tag0}');
|
|
56
|
+
expect(result.tags[0].space).toBeNull();
|
|
57
|
+
expect(result.tags[0].name).toBe('I Am Long');
|
|
58
|
+
expect(result.tags[0].includeOwner).toBe(false);
|
|
59
|
+
expect(result.tags[0].searchDepth).toBe(1);
|
|
60
|
+
expect(result.tags[0].strictSearchDepth).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('identifyTags can identify tag spaces', () => {
|
|
64
|
+
const result = splitQuery('#Space1.Tag1 AND #[Space 2.Tag 2]');
|
|
65
|
+
|
|
66
|
+
result.where = identifyTags(result.where, result);
|
|
67
|
+
|
|
68
|
+
expect(result.where).toBe('{tag0} AND {tag1}');
|
|
69
|
+
expect(result.tags[0].space).toBe('Space1');
|
|
70
|
+
expect(result.tags[0].name).toBe('Tag1');
|
|
71
|
+
expect(result.tags[0].includeOwner).toBe(false);
|
|
72
|
+
expect(result.tags[0].searchDepth).toBe(1);
|
|
73
|
+
expect(result.tags[0].strictSearchDepth).toBe(true);
|
|
74
|
+
expect(result.tags[1].space).toBe('Space 2');
|
|
75
|
+
expect(result.tags[1].name).toBe('Tag 2');
|
|
76
|
+
expect(result.tags[1].includeOwner).toBe(false);
|
|
77
|
+
expect(result.tags[1].searchDepth).toBe(1);
|
|
78
|
+
expect(result.tags[1].strictSearchDepth).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('identifyAttrs can correctly identify multiple attrs in query', () => {
|
|
82
|
+
const result = splitQuery('@Count > 3 AND @Depth < 4');
|
|
83
|
+
|
|
84
|
+
result.where = identifyAttrs(result.where, result);
|
|
85
|
+
|
|
86
|
+
expect(result.where).toBe('{attr0} > 3 AND {attr1} < 4');
|
|
87
|
+
expect(result.attrs[0].space).toBeNull();
|
|
88
|
+
expect(result.attrs[0].name).toBe('Count');
|
|
89
|
+
expect(result.attrs[0].exists).toBe(false);
|
|
90
|
+
expect(result.attrs[1].space).toBeNull();
|
|
91
|
+
expect(result.attrs[1].name).toBe('Depth');
|
|
92
|
+
expect(result.attrs[1].exists).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('identifyAttrs can identify space names', () => {
|
|
96
|
+
const result = splitQuery('@MySpace.Count = 123');
|
|
97
|
+
|
|
98
|
+
result.where = identifyAttrs(result.where, result);
|
|
99
|
+
|
|
100
|
+
expect(result.where).toBe('{attr0} = 123');
|
|
101
|
+
expect(result.attrs[0].space).toBe('MySpace');
|
|
102
|
+
expect(result.attrs[0].name).toBe('Count');
|
|
103
|
+
expect(result.attrs[0].exists).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('identifyAttrs can identify exists queries', () => {
|
|
107
|
+
const result = splitQuery('@Help.Exists()');
|
|
108
|
+
|
|
109
|
+
result.where = identifyAttrs(result.where, result);
|
|
110
|
+
|
|
111
|
+
expect(result.where).toBe('{attr0}');
|
|
112
|
+
expect(result.attrs[0].space).toBeNull();
|
|
113
|
+
expect(result.attrs[0].name).toBe('Help');
|
|
114
|
+
expect(result.attrs[0].exists).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('identifyAttrs can identify on tag filters', () => {
|
|
118
|
+
const result = splitQuery('@Abc.On(MyTag) > 1');
|
|
119
|
+
|
|
120
|
+
result.where = identifyAttrs(result.where, result);
|
|
121
|
+
|
|
122
|
+
expect(result.where).toBe('{attr0} > 1');
|
|
123
|
+
expect(result.attrs[0].name).toBe('Abc');
|
|
124
|
+
expect(result.attrs[0].exists).toBe(false);
|
|
125
|
+
expect(result.attrs[0].tagNameFilters[0].name).toBe('MyTag');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('identifyAttrs can handle attr and space names with spaces in them', () => {
|
|
129
|
+
const result = splitQuery('@[My Space.Test Test] = 123');
|
|
130
|
+
|
|
131
|
+
result.where = identifyAttrs(result.where, result);
|
|
132
|
+
|
|
133
|
+
expect(result.where).toBe('{attr0} = 123');
|
|
134
|
+
expect(result.attrs[0].space).toBe('My Space');
|
|
135
|
+
expect(result.attrs[0].name).toBe('Test Test');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('identifyAttrs can support multiple pipe-separated on(tag) filters', () => {
|
|
139
|
+
const result = splitQuery('@Abc.Exists().On(Tag1|#Space2.Tag2)');
|
|
140
|
+
|
|
141
|
+
result.where = identifyAttrs(result.where, result);
|
|
142
|
+
|
|
143
|
+
expect(result.where).toBe('{attr0}');
|
|
144
|
+
expect(result.attrs[0].name).toBe('Abc');
|
|
145
|
+
expect(result.attrs[0].exists).toBe(true);
|
|
146
|
+
expect(result.attrs[0].tagNameFilters.length).toBe(2);
|
|
147
|
+
expect(result.attrs[0].tagNameFilters[0].name).toBe('Tag1');
|
|
148
|
+
expect(result.attrs[0].tagNameFilters[0].space).toBeNull();
|
|
149
|
+
expect(result.attrs[0].tagNameFilters[0].searchDepth).toBe(0);
|
|
150
|
+
expect(result.attrs[0].tagNameFilters[1].name).toBe('Tag2');
|
|
151
|
+
expect(result.attrs[0].tagNameFilters[1].space).toBe('Space2');
|
|
152
|
+
expect(result.attrs[0].tagNameFilters[1].searchDepth).toBe(1);
|
|
153
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export class ParsedQuery {
|
|
5
|
+
where: string = null;
|
|
6
|
+
order: string = null;
|
|
7
|
+
tags: Array<ParsedTag> = [];
|
|
8
|
+
attrs: Array<ParsedAttr> = [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class ParsedTag {
|
|
12
|
+
space: string = null;
|
|
13
|
+
name: string = null;
|
|
14
|
+
searchDepth: number = 0;
|
|
15
|
+
strictSearchDepth: boolean = true;
|
|
16
|
+
includeOwner: boolean = false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class ParsedAttr {
|
|
20
|
+
space: string = null;
|
|
21
|
+
name: string = null;
|
|
22
|
+
exists: boolean = false;
|
|
23
|
+
tagNameFilters: Array<ParsedTag> = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
export default function parseQuery(query: string): ParsedQuery {
|
|
28
|
+
const output = splitQuery(query);
|
|
29
|
+
|
|
30
|
+
output.where = identifyTags(output.where, output);
|
|
31
|
+
output.order = identifyTags(output.order, output);
|
|
32
|
+
|
|
33
|
+
output.where = identifyAttrs(output.where, output);
|
|
34
|
+
output.order = identifyAttrs(output.order, output);
|
|
35
|
+
|
|
36
|
+
return output;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
export function splitQuery(query: string): ParsedQuery {
|
|
41
|
+
query = ' ' + query + ' ';
|
|
42
|
+
const output = new ParsedQuery();
|
|
43
|
+
|
|
44
|
+
const orderByIndex = query.toUpperCase().indexOf(' ORDER BY ');
|
|
45
|
+
if (orderByIndex < 0) {
|
|
46
|
+
output.where = query.trim();
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
output.where = query.substring(0, orderByIndex).trim();
|
|
50
|
+
output.order = query.substring(orderByIndex + ' ORDER BY '.length).trim();
|
|
51
|
+
}
|
|
52
|
+
if (output.where == '')
|
|
53
|
+
output.where = null;
|
|
54
|
+
return output;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
export function identifyTags(query: string, parsedQuery: ParsedQuery): string {
|
|
59
|
+
const regexes: Array<RegExp> = [
|
|
60
|
+
/(#+\??~?|~)([\w\d]+\.)?([\w\d]+)/, //Single word tags and space names
|
|
61
|
+
/(#+\??~?|~)\[([\w\d\s]+\.)?([\w\d\s]+)\]/ //Multi-word tags and space names wrapped in []
|
|
62
|
+
];
|
|
63
|
+
for (const regex of regexes) {
|
|
64
|
+
while (true) {
|
|
65
|
+
const match = regex.exec(query);
|
|
66
|
+
if (!match)
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
const hashPrefix = match[1];
|
|
70
|
+
const parsedTag = new ParsedTag();
|
|
71
|
+
parsedTag.space = !!match[2] ? match[2].substring(0, match[2].length - 1) : null;
|
|
72
|
+
parsedTag.name = match[3];
|
|
73
|
+
parsedTag.includeOwner = hashPrefix.includes('~');
|
|
74
|
+
parsedTag.searchDepth = (hashPrefix.match(/#/g)||[]).length;
|
|
75
|
+
parsedTag.strictSearchDepth = !hashPrefix.includes('?');
|
|
76
|
+
|
|
77
|
+
const fullMatch = match[0];
|
|
78
|
+
const matchStart = query.indexOf(fullMatch);
|
|
79
|
+
const matchEnd = matchStart + fullMatch.length;
|
|
80
|
+
query = query.substring(0, matchStart) + `{tag${parsedQuery.tags.length}}` + query.substring(matchEnd);
|
|
81
|
+
parsedQuery.tags.push(parsedTag);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return query;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
export function identifyAttrs(query: string, parsedQuery: ParsedQuery): string {
|
|
89
|
+
const regexes: Array<RegExp> = [
|
|
90
|
+
/@([\w\d]+\.(?!Exists\(|On\())?([\w\d]+)/,
|
|
91
|
+
/@\[([\w\d\s]+\.)?([\w\d\s]+)\]/
|
|
92
|
+
];
|
|
93
|
+
for (const regex of regexes) {
|
|
94
|
+
while (true) {
|
|
95
|
+
//If no more matches to be found, continue to next regex test
|
|
96
|
+
const match = regex.exec(query);
|
|
97
|
+
if (!match)
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
//Build up basic properties of ParsedAttr object
|
|
101
|
+
const parsedAttr = new ParsedAttr();
|
|
102
|
+
parsedAttr.space = !!match[1] ? match[1].substring(0, match[1].length - 1) : null;
|
|
103
|
+
parsedAttr.name = match[2];
|
|
104
|
+
|
|
105
|
+
//Record the positions of where the match starts and ends
|
|
106
|
+
const matchStart = query.indexOf(match[0]);
|
|
107
|
+
let matchEnd = matchStart + match[0].length;
|
|
108
|
+
|
|
109
|
+
//Check if there's a test for existance of the attribute on notes
|
|
110
|
+
if (query.substring(matchEnd, matchEnd + '.Exists()'.length) == '.Exists()') {
|
|
111
|
+
parsedAttr.exists = true;
|
|
112
|
+
matchEnd += '.Exists()'.length;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//Check if there's any conditions about which tags to look for the attribute on
|
|
116
|
+
if (query.substring(matchEnd, matchEnd + '.On('.length) == '.On(') {
|
|
117
|
+
let tagFilterStart = matchEnd + '.On('.length;
|
|
118
|
+
matchEnd = query.indexOf(')', tagFilterStart);
|
|
119
|
+
if (matchEnd < 0)
|
|
120
|
+
throw Error('Unclosed bracket detected');
|
|
121
|
+
let tagNameFilters = query.substring(tagFilterStart, matchEnd).split('|');
|
|
122
|
+
const dummyParsedQuery = new ParsedQuery();
|
|
123
|
+
for (let tagNameFilter of tagNameFilters) {
|
|
124
|
+
if (!tagNameFilter.startsWith('~'))
|
|
125
|
+
tagNameFilter = '~' + tagNameFilter;
|
|
126
|
+
identifyTags(tagNameFilter, dummyParsedQuery);
|
|
127
|
+
}
|
|
128
|
+
parsedAttr.tagNameFilters = dummyParsedQuery.tags;
|
|
129
|
+
matchEnd++;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
query = query.substring(0, matchStart) + `{attr${parsedQuery.attrs.length}}` + query.substring(matchEnd);
|
|
133
|
+
parsedQuery.attrs.push(parsedAttr);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return query;
|
|
137
|
+
}
|