notu 0.1.0 → 0.2.0

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.
@@ -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
+ }