@kwiz/node 1.0.8 → 1.0.10
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/__azurite_db_blob__.json +1 -0
- package/__azurite_db_blob_extent__.json +1 -0
- package/__azurite_db_queue__.json +1 -0
- package/__azurite_db_queue_extent__.json +1 -0
- package/__azurite_db_table__.json +1 -0
- package/package.json +5 -2
- package/src/exports-index.ts +1 -0
- package/src/storage/exports-index.ts +2 -0
- package/src/storage/odata.ts +87 -0
- package/src/storage/table-storage.test.js +129 -0
- package/src/storage/table-storage.ts +314 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"filename":"C:\\Repos\\@kwiz\\node\\__azurite_db_blob__.json","collections":[{"name":"$SERVICES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{},"constraints":null,"uniqueNames":["accountName"],"transforms":{},"objType":"$SERVICES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$CONTAINERS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$CONTAINERS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$BLOBS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"containerName":{"name":"containerName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]},"snapshot":{"name":"snapshot","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$BLOBS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$BLOCKS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"containerName":{"name":"containerName","dirty":false,"values":[]},"blobName":{"name":"blobName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$BLOCKS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"filename":"C:\\Repos\\@kwiz\\node\\__azurite_db_blob_extent__.json","collections":[{"name":"$EXTENTS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"id":{"name":"id","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$EXTENTS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"filename":"C:\\Repos\\@kwiz\\node\\__azurite_db_queue__.json","collections":[{"name":"$SERVICES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{},"constraints":null,"uniqueNames":["accountName"],"transforms":{},"objType":"$SERVICES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$QUEUES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$QUEUES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$MESSAGES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"queueName":{"name":"queueName","dirty":false,"values":[]},"messageId":{"name":"messageId","dirty":false,"values":[]},"visibleTime":{"name":"visibleTime","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$MESSAGES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"filename":"C:\\Repos\\@kwiz\\node\\__azurite_db_queue_extent__.json","collections":[{"name":"$EXTENTS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"id":{"name":"id","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$EXTENTS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"filename":"C:\\Repos\\@kwiz\\node\\__azurite_db_table__.json","collections":[{"name":"$TABLES_COLLECTION$","data":[],"idIndex":[],"binaryIndices":{"account":{"name":"account","dirty":false,"values":[]},"table":{"name":"table","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$TABLES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":5,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$SERVICES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{},"constraints":null,"uniqueNames":["accountName"],"transforms":{},"objType":"$SERVICES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kwiz/node",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
4
4
|
"description": "KWIZ utilities and helpers for node applications",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
"npm-publish": "npm publish --access public",
|
|
25
25
|
"reset-repo": "git fetch origin && git reset --hard origin/main",
|
|
26
26
|
"link-local-common": "npm link @kwiz/common",
|
|
27
|
-
"reindex-project": "cti create ./src -i _dependencies -w -b -n -o exports-index.ts && node fix-folder-imports.js"
|
|
27
|
+
"reindex-project": "cti create ./src -i _dependencies -w -b -n -o exports-index.ts && node fix-folder-imports.js",
|
|
28
|
+
"install-packages": "npm install azurite -g && npm ci",
|
|
29
|
+
"startDevStorage": "azurite -s -l"
|
|
28
30
|
},
|
|
29
31
|
"repository": {
|
|
30
32
|
"type": "git",
|
|
@@ -64,6 +66,7 @@
|
|
|
64
66
|
"typescript": "^5.3.3"
|
|
65
67
|
},
|
|
66
68
|
"dependencies": {
|
|
69
|
+
"@azure/data-tables": "^13.2.2",
|
|
67
70
|
"@azure/msal-node": "^2.6.4",
|
|
68
71
|
"@kwiz/common": "^1.0.10",
|
|
69
72
|
"axios": "^1.6.7",
|
package/src/exports-index.ts
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { isNullOrEmptyArray, isNullOrEmptyString, isNullOrUndefined } from "@kwiz/common";
|
|
2
|
+
|
|
3
|
+
export enum ODataOperators {
|
|
4
|
+
equal = "eq",
|
|
5
|
+
notEqual = "ne",
|
|
6
|
+
greater = "gt",
|
|
7
|
+
greaterOrEqual = "ge",
|
|
8
|
+
less = "lt",
|
|
9
|
+
lessOrEqual = "le",
|
|
10
|
+
contains = "in",
|
|
11
|
+
startswith = "startswith"
|
|
12
|
+
}
|
|
13
|
+
export enum ODataJoinOperators {
|
|
14
|
+
and = "and",
|
|
15
|
+
or = "or"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface IOdataFilter<DataType> {
|
|
19
|
+
property: keyof DataType;
|
|
20
|
+
operator: ODataOperators;
|
|
21
|
+
value: string | Date | number | boolean;
|
|
22
|
+
not?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface IOdataFilterStatement<DataType> {
|
|
25
|
+
filters: (IOdataFilter<DataType> | IOdataFilterStatement<DataType>)[];
|
|
26
|
+
/** default: or */
|
|
27
|
+
join?: ODataJoinOperators;
|
|
28
|
+
not?: boolean;
|
|
29
|
+
}
|
|
30
|
+
function isIOdataFilter<DataType>(f: IOdataFilter<DataType> | IOdataFilterStatement<DataType>): f is IOdataFilter<DataType> {
|
|
31
|
+
if (typeof (f as IOdataFilter<DataType>).property === "string")
|
|
32
|
+
return true;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
function isIOdataFilterStatement<DataType>(f: IOdataFilter<DataType> | IOdataFilterStatement<DataType>): f is IOdataFilter<DataType> {
|
|
36
|
+
if (typeof (f as IOdataFilterStatement<DataType>).filters === "object" && Array.isArray((f as IOdataFilterStatement<DataType>).filters))
|
|
37
|
+
return true;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
enum TableStorageKnownColumns {
|
|
42
|
+
partitionKey = "PartitionKey",
|
|
43
|
+
rowKey = "RowKey"
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getPropNameForFilter(prop: string | number | symbol) {
|
|
47
|
+
return TableStorageKnownColumns[prop] || prop as string;
|
|
48
|
+
}
|
|
49
|
+
function getOdataFilterStatement<DataType>(filter: IOdataFilter<DataType>) {
|
|
50
|
+
let filterValue = isNullOrUndefined(filter.value)
|
|
51
|
+
? `null`
|
|
52
|
+
: typeof filter.value === "string"
|
|
53
|
+
? `'${filter.value.replace(/'/g, "''")}'`
|
|
54
|
+
: filter.value instanceof Date
|
|
55
|
+
? `datetime'${filter.value.toISOString()}'`
|
|
56
|
+
: `${filter.value}`;
|
|
57
|
+
|
|
58
|
+
if (filter.operator === ODataOperators.startswith)
|
|
59
|
+
return `${filter.not ? 'not ' : ''}${filter.operator}(${getPropNameForFilter(filter.property)}, ${filterValue})`;
|
|
60
|
+
return `${filter.not ? 'not ' : ''}${getPropNameForFilter(filter.property)} ${filter.operator} ${filterValue}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getOdataFilter<DataType>(statement: IOdataFilterStatement<DataType>) {
|
|
64
|
+
if (isNullOrUndefined(statement) || isNullOrEmptyArray(statement.filters)) return "";
|
|
65
|
+
if (isNullOrEmptyString(statement.join)) statement.join = ODataJoinOperators.or;
|
|
66
|
+
|
|
67
|
+
let filterStatements: string[] = [];
|
|
68
|
+
statement.filters.forEach(filter => {
|
|
69
|
+
if (isIOdataFilter(filter)) {
|
|
70
|
+
filterStatements.push(getOdataFilterStatement<DataType>(filter));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
let subStatement = getOdataFilter(filter);
|
|
74
|
+
if (!isNullOrEmptyString(subStatement))
|
|
75
|
+
filterStatements.push(subStatement);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
if (filterStatements.length === 0) return "";
|
|
79
|
+
|
|
80
|
+
let result = "";
|
|
81
|
+
if (filterStatements.length === 1) result = filterStatements[0];
|
|
82
|
+
else if (filterStatements.length > 1) {
|
|
83
|
+
result = `(${filterStatements.join(` ${statement.join} `)})`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return `${statement.not ? '(not ' : ''}${result}${statement.not ? ')' : ''}`;
|
|
87
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import assert from 'assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { ODataOperators } from "./odata";
|
|
4
|
+
import { ConfigureTableStorage, Table, addItem, ensureTable } from "./table-storage";
|
|
5
|
+
|
|
6
|
+
test('testTableStorage', async t => {
|
|
7
|
+
//configure storage
|
|
8
|
+
ConfigureTableStorage({ connectionString: "UseDevelopmentStorage=true" });
|
|
9
|
+
|
|
10
|
+
const tableName = "TestTable";
|
|
11
|
+
const table = new Table(tableName, {
|
|
12
|
+
getKeys: p => p,
|
|
13
|
+
transform: {
|
|
14
|
+
load: v => ({ ...v, schedule: JSON.parse(v.schedule) }),
|
|
15
|
+
save: v => ({ ...v, schedule: JSON.stringify(v.schedule) })
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const partitionKey = "PK1";
|
|
19
|
+
const partitionKey2 = "PK2";
|
|
20
|
+
const rowKey1 = "0b8612bb-377d-4897-be8f-a532a1ad3e73|d2e32b93-c096-431c-8a79-fcce9a4d7bce";
|
|
21
|
+
const rowKey2 = "0b8612bb-377d-4897-be8f-a532a1ad3e73|cd24aa15-0d03-4892-bb46-af32e5fb9361";
|
|
22
|
+
//start fresh
|
|
23
|
+
let result = await table.delete();
|
|
24
|
+
await t.test("Delete table", t => assert.strictEqual(result, true));
|
|
25
|
+
|
|
26
|
+
//provoke table name not allowed
|
|
27
|
+
result = await ensureTable("T2");
|
|
28
|
+
await t.test("Table name not allowed", t => assert.strictEqual(result, false));
|
|
29
|
+
|
|
30
|
+
//create table
|
|
31
|
+
result = await table.ensure();
|
|
32
|
+
await t.test("Create table", t => assert.strictEqual(result, true));
|
|
33
|
+
|
|
34
|
+
//provoke table already exists
|
|
35
|
+
result = await table.ensure();
|
|
36
|
+
await t.test("Ensure existing table", t => assert.strictEqual(result, true));
|
|
37
|
+
|
|
38
|
+
//add item
|
|
39
|
+
result = await table.addItem({
|
|
40
|
+
partitionKey: partitionKey,
|
|
41
|
+
rowKey: rowKey1,
|
|
42
|
+
list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
|
|
43
|
+
action: "6b964dd7-6e62-42f6-89e4-5e79d6b6cbb0",
|
|
44
|
+
nextUTC: "2023010105",
|
|
45
|
+
schedule: { days: 4, hours: [1, 2, 3] }
|
|
46
|
+
});
|
|
47
|
+
await t.test("Create item 1", t => assert.strictEqual(result, true));
|
|
48
|
+
|
|
49
|
+
result = await table.addItem({
|
|
50
|
+
partitionKey: partitionKey,
|
|
51
|
+
rowKey: rowKey2,
|
|
52
|
+
list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
|
|
53
|
+
action: "fab4094b-c000-4ff4-bb52-0f8538b2bc49",
|
|
54
|
+
nextUTC: "2023010105",
|
|
55
|
+
schedule: { days: 4, hours: [1, 2, 3] }
|
|
56
|
+
});
|
|
57
|
+
await t.test("Create item 2", t => assert.strictEqual(result, true));
|
|
58
|
+
|
|
59
|
+
result = await table.addItem({
|
|
60
|
+
partitionKey: partitionKey2,
|
|
61
|
+
rowKey: rowKey1,
|
|
62
|
+
list: "b98211e2-809a-4761-b928-559eddc28dfc",
|
|
63
|
+
action: "d20196e5-feb9-4202-bd03-718b49bf029e",
|
|
64
|
+
nextUTC: "2023010105",
|
|
65
|
+
schedule: { days: 4, hours: [1, 2, 3] }
|
|
66
|
+
});
|
|
67
|
+
await t.test("Create item in second partition", t => assert.strictEqual(result, true));
|
|
68
|
+
|
|
69
|
+
//provoke add second item with same key
|
|
70
|
+
result = await table.addItem({
|
|
71
|
+
partitionKey: partitionKey,
|
|
72
|
+
rowKey: rowKey1,
|
|
73
|
+
list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
|
|
74
|
+
action: "6b964dd7-6e62-42f6-89e4-5e79d6b6cbb0",
|
|
75
|
+
nextUTC: "2023010105",
|
|
76
|
+
schedule: { days: 4, hours: [1, 2, 3] }
|
|
77
|
+
});
|
|
78
|
+
await t.test("Create item with existing key", t => assert.strictEqual(result, false));
|
|
79
|
+
|
|
80
|
+
//provoke add second item with same key
|
|
81
|
+
result = await table.upsertItem({
|
|
82
|
+
partitionKey: partitionKey,
|
|
83
|
+
rowKey: rowKey1,
|
|
84
|
+
list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
|
|
85
|
+
action: "6b964dd7-6e62-42f6-89e4-5e79d6b6cbb0",
|
|
86
|
+
nextUTC: "2023020202",
|
|
87
|
+
schedule: { days: 4, hours: [1, 2, 3] }
|
|
88
|
+
});
|
|
89
|
+
await t.test("Update item 1", t => assert.strictEqual(result, true));
|
|
90
|
+
|
|
91
|
+
//provoke add item to table that does not exist
|
|
92
|
+
result = await addItem(tableName + "DoeNotExist", {
|
|
93
|
+
partitionKey: partitionKey,
|
|
94
|
+
rowKey: rowKey1,
|
|
95
|
+
list: "1bd32599-375e-4c98-9e1c-ce9dd2f2e17d",
|
|
96
|
+
action: "6b964dd7-6e62-42f6-89e4-5e79d6b6cbb0",
|
|
97
|
+
nextUTC: "2023020202",
|
|
98
|
+
schedule: { days: 4, hours: [1, 2, 3] }
|
|
99
|
+
});
|
|
100
|
+
await t.test("Create item in table that does not exist", t => assert.strictEqual(result, false));
|
|
101
|
+
|
|
102
|
+
let allItems = await table.getItems();
|
|
103
|
+
await t.test("Get items", t => assert.strictEqual(allItems.length, 3));
|
|
104
|
+
|
|
105
|
+
//find items by partition
|
|
106
|
+
let partitionItems = await table.getItems({
|
|
107
|
+
filterStatment: {
|
|
108
|
+
filters: [
|
|
109
|
+
{
|
|
110
|
+
property: "partitionKey",
|
|
111
|
+
operator: ODataOperators.equal,
|
|
112
|
+
value: partitionKey
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await t.test("Get items with filter", t => assert.strictEqual(partitionItems.length, 2));
|
|
119
|
+
await t.test("Update values", t => assert.strictEqual(partitionItems[1].nextUTC, "2023020202"));
|
|
120
|
+
|
|
121
|
+
let deleteResult = await table.deleteItem({ partitionKey: partitionKey, rowKey: rowKey1 });
|
|
122
|
+
await t.test("delete item", t => assert.strictEqual(deleteResult, true));
|
|
123
|
+
deleteResult = await table.deleteItem({ partitionKey: partitionKey, rowKey: rowKey1 });
|
|
124
|
+
await t.test("delete item that was already deleted", t => assert.strictEqual(deleteResult, true));
|
|
125
|
+
|
|
126
|
+
//cleanup
|
|
127
|
+
result = await table.delete();
|
|
128
|
+
await t.test("Delete table", t => assert.strictEqual(result, true));
|
|
129
|
+
});
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
//https://www.npmjs.com/package/@azure/storage-blob
|
|
2
|
+
//https://www.npmjs.com/package/@azure/storage-queue
|
|
3
|
+
//https://www.npmjs.com/package/@azure/storage-file-share
|
|
4
|
+
//https://www.npmjs.com/package/@azure/data-tables
|
|
5
|
+
//https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio-code
|
|
6
|
+
import { FullOperationResponse } from "@azure/core-client";
|
|
7
|
+
import { ListTableEntitiesOptions, TableClient, TableServiceClient, UpdateMode, odata } from "@azure/data-tables";
|
|
8
|
+
import { isNullOrEmptyString, isNullOrUndefined } from "@kwiz/common";
|
|
9
|
+
import { IOdataFilterStatement, getOdataFilter } from "./odata";
|
|
10
|
+
|
|
11
|
+
var connectionString: string = null;
|
|
12
|
+
export function ConfigureTableStorage(config: { connectionString: string; }) {
|
|
13
|
+
connectionString = config.connectionString;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getTableService() {
|
|
17
|
+
if (isNullOrEmptyString(connectionString)) throw Error("Call ConfigureTableStorage first");
|
|
18
|
+
return TableServiceClient.fromConnectionString(connectionString);
|
|
19
|
+
}
|
|
20
|
+
function getTableClient(tableName: string) {
|
|
21
|
+
if (isNullOrEmptyString(connectionString)) throw Error("Call ConfigureTableStorage first");
|
|
22
|
+
return TableClient.fromConnectionString(connectionString, tableName);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface IODataError {
|
|
26
|
+
odataError: {
|
|
27
|
+
code: string | "TableAlreadyExists" | "EntityAlreadyExists" | "TableNotFound" | "",
|
|
28
|
+
message: {
|
|
29
|
+
lang: string | "en-US",
|
|
30
|
+
value: string
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type TableEntityBase = {
|
|
36
|
+
/**
|
|
37
|
+
* The PartitionKey property of the entity.
|
|
38
|
+
* Does not allow / \ # ? any control characted (like \n)
|
|
39
|
+
*/
|
|
40
|
+
partitionKey: string;
|
|
41
|
+
/**
|
|
42
|
+
* The RowKey property of the entity.
|
|
43
|
+
*/
|
|
44
|
+
rowKey: string;
|
|
45
|
+
}
|
|
46
|
+
//correctly limits the column types...
|
|
47
|
+
export type TableEntityType<DataType extends TableEntityBase> = {
|
|
48
|
+
[P in keyof DataType]: string | boolean | Date | number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export async function findTable(tableName: string) {
|
|
52
|
+
const tableService = getTableService();
|
|
53
|
+
|
|
54
|
+
let found = false;
|
|
55
|
+
try {
|
|
56
|
+
const tables = tableService.listTables({
|
|
57
|
+
queryOptions: {
|
|
58
|
+
//Tag function - read more https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
|
|
59
|
+
filter: odata`TableName eq ${tableName}`
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
for await (const table of tables) {
|
|
63
|
+
found = true;
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
//console.log(e);
|
|
67
|
+
}
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function listTables() {
|
|
72
|
+
const tableService = getTableService();
|
|
73
|
+
|
|
74
|
+
let arr: string[] = [];
|
|
75
|
+
try {
|
|
76
|
+
const tables = tableService.listTables();
|
|
77
|
+
for await (const table of tables) {
|
|
78
|
+
arr.push(table.name);
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
//console.log(e);
|
|
82
|
+
}
|
|
83
|
+
return arr;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// async function ensureTableObsolete(name: string) {
|
|
87
|
+
// const tableService = getTableService();
|
|
88
|
+
// //create table
|
|
89
|
+
// // If the table 'newTable' already exists, createTable doesn't throw
|
|
90
|
+
// let success = false;
|
|
91
|
+
// try {
|
|
92
|
+
// await tableService.createTable(name, {
|
|
93
|
+
// onResponse: raw => {
|
|
94
|
+
// let error = isError(raw);
|
|
95
|
+
// success = error.isError !== true;
|
|
96
|
+
// if (error.isError) console.log(error.message);
|
|
97
|
+
// }
|
|
98
|
+
// });
|
|
99
|
+
// }
|
|
100
|
+
// catch (e) {
|
|
101
|
+
// success = false;
|
|
102
|
+
// }
|
|
103
|
+
|
|
104
|
+
// return success;
|
|
105
|
+
// }
|
|
106
|
+
|
|
107
|
+
export async function ensureTable(tableName: string) {
|
|
108
|
+
const table = getTableClient(tableName);
|
|
109
|
+
let success = false;
|
|
110
|
+
try {
|
|
111
|
+
await table.createTable({
|
|
112
|
+
onResponse: raw => {
|
|
113
|
+
let error = isError(raw);
|
|
114
|
+
success = error.isError !== true || error.message === "TableAlreadyExists";
|
|
115
|
+
//if (error.isError) console.error(error.message);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
} catch (e) {
|
|
119
|
+
//console.error(e);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return success;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function deleteTable(tableName: string) {
|
|
126
|
+
const table = getTableClient(tableName);
|
|
127
|
+
let success = false;
|
|
128
|
+
try {
|
|
129
|
+
await table.deleteTable({
|
|
130
|
+
onResponse: raw => {
|
|
131
|
+
let error = isError(raw);
|
|
132
|
+
success = error.isError !== true;
|
|
133
|
+
//if (error.isError) console.log(error.message);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
} catch (e) {
|
|
137
|
+
//console.log(e);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return success;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
//https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/tables/data-tables/samples/v13/typescript/src/queryEntities.ts
|
|
144
|
+
export async function getItems<DataType extends TableEntityBase & TableEntityType<DataType>>(tableName: string, options?: {
|
|
145
|
+
filterStatment?: IOdataFilterStatement<DataType>;
|
|
146
|
+
postFilter?: (item: DataType) => boolean;
|
|
147
|
+
}) {
|
|
148
|
+
const table = getTableClient(tableName);
|
|
149
|
+
let result: DataType[] = [];
|
|
150
|
+
try {
|
|
151
|
+
let o: ListTableEntitiesOptions;
|
|
152
|
+
if (options) {
|
|
153
|
+
if (!isNullOrUndefined(options.filterStatment)) {
|
|
154
|
+
let filterStatment = getOdataFilter(options.filterStatment);
|
|
155
|
+
if (!isNullOrEmptyString(filterStatment))
|
|
156
|
+
o = {
|
|
157
|
+
queryOptions: {
|
|
158
|
+
filter: filterStatment
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
let items = table.listEntities<DataType>(o);
|
|
164
|
+
for await (const item of items) {
|
|
165
|
+
//console.dir(item);
|
|
166
|
+
if (!options || typeof options.postFilter !== "function" || options.postFilter(item))
|
|
167
|
+
result.push(item as DataType);
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {
|
|
170
|
+
//console.log(e);
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function addItem<DataType extends TableEntityBase & TableEntityType<DataType>>(tableName: string, item: DataType) {
|
|
176
|
+
const table = getTableClient(tableName);
|
|
177
|
+
let success = false;
|
|
178
|
+
try {
|
|
179
|
+
let result = await table.createEntity(item, {
|
|
180
|
+
onResponse: raw => {
|
|
181
|
+
let error = isError(raw);
|
|
182
|
+
success = error.isError !== true;
|
|
183
|
+
//if (error.isError) console.log(error.message);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
//console.log(result);
|
|
187
|
+
success = true;
|
|
188
|
+
} catch (e) { success = false; }
|
|
189
|
+
return success;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function deleteItem(tableName: string, partitionKey: string, rowKey: string) {
|
|
193
|
+
const table = getTableClient(tableName);
|
|
194
|
+
let success = false;
|
|
195
|
+
try {
|
|
196
|
+
let result = await table.deleteEntity(partitionKey, rowKey, {
|
|
197
|
+
onResponse: raw => {
|
|
198
|
+
let error = isError(raw);
|
|
199
|
+
success = error.isError !== true || error.message === "ResourceNotFound";
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
//console.log(result);
|
|
203
|
+
success = true;
|
|
204
|
+
} catch (e) {
|
|
205
|
+
//success = false;
|
|
206
|
+
}
|
|
207
|
+
return success;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function upsertItem<DataType extends TableEntityBase & TableEntityType<DataType>>(tableName: string, item: DataType, options?: {
|
|
211
|
+
mode?: UpdateMode
|
|
212
|
+
}) {
|
|
213
|
+
const table = getTableClient(tableName);
|
|
214
|
+
let success = false;
|
|
215
|
+
try {
|
|
216
|
+
let result = await table.upsertEntity(item, options?.mode || "Replace", {
|
|
217
|
+
onResponse: raw => {
|
|
218
|
+
let error = isError(raw);
|
|
219
|
+
success = error.isError !== true;
|
|
220
|
+
//if (error.isError) console.log(error.message);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
//console.log(result);
|
|
224
|
+
success = true;
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.error(e);
|
|
227
|
+
success = false;
|
|
228
|
+
}
|
|
229
|
+
return success;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export class Table<KeysType extends TableEntityBase,
|
|
233
|
+
GetKeysParam,
|
|
234
|
+
SavedRow extends KeysType & TableEntityType<SavedRow>,
|
|
235
|
+
ParsedRow = SavedRow>{
|
|
236
|
+
private tableName: string;
|
|
237
|
+
private transform: {
|
|
238
|
+
save: (parsed: ParsedRow, table: Table<KeysType, GetKeysParam, SavedRow, ParsedRow>) => SavedRow;
|
|
239
|
+
load: (saved: SavedRow, table: Table<KeysType, GetKeysParam, SavedRow, ParsedRow>) => ParsedRow;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
public getKeys: (p: GetKeysParam) => KeysType = null;
|
|
243
|
+
|
|
244
|
+
/** If your type contains complex values, provide a transforer to serialize/deserialize those complex columns */
|
|
245
|
+
public constructor(tableName: string, options: {
|
|
246
|
+
getKeys: (p: GetKeysParam) => KeysType,
|
|
247
|
+
transform?: {
|
|
248
|
+
save: (parsed: ParsedRow, table: Table<KeysType, GetKeysParam, SavedRow, ParsedRow>) => SavedRow;
|
|
249
|
+
load: (saved: SavedRow, table: Table<KeysType, GetKeysParam, SavedRow, ParsedRow>) => ParsedRow;
|
|
250
|
+
}
|
|
251
|
+
}) {
|
|
252
|
+
this.tableName = tableName;
|
|
253
|
+
this.getKeys = options.getKeys;
|
|
254
|
+
this.transform = options.transform || {
|
|
255
|
+
save: v => v as any as SavedRow,
|
|
256
|
+
load: v => v as any as ParsedRow
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
public delete() {
|
|
260
|
+
return deleteTable(this.tableName);
|
|
261
|
+
}
|
|
262
|
+
public ensure() {
|
|
263
|
+
return ensureTable(this.tableName);
|
|
264
|
+
}
|
|
265
|
+
public async getItems(options?: {
|
|
266
|
+
filterStatment?: IOdataFilterStatement<SavedRow>;
|
|
267
|
+
postFilter?: (item: SavedRow) => boolean;
|
|
268
|
+
}) {
|
|
269
|
+
let items = await getItems<SavedRow>(this.tableName, options);
|
|
270
|
+
return items.map(i => this.transform.load(i, this));
|
|
271
|
+
}
|
|
272
|
+
public async addItem(item: ParsedRow) {
|
|
273
|
+
await this.ensure();
|
|
274
|
+
|
|
275
|
+
return addItem<SavedRow>(this.tableName, this.transform.save(item, this));
|
|
276
|
+
}
|
|
277
|
+
public async upsertItem(item: ParsedRow, options?: {
|
|
278
|
+
mode?: UpdateMode
|
|
279
|
+
}) {
|
|
280
|
+
await this.ensure();
|
|
281
|
+
|
|
282
|
+
return upsertItem<SavedRow>(this.tableName, this.transform.save(item, this), options);
|
|
283
|
+
}
|
|
284
|
+
public deleteItemByKey(param: GetKeysParam) {
|
|
285
|
+
let keys = this.getKeys(param);
|
|
286
|
+
return this.deleteItem(keys);
|
|
287
|
+
}
|
|
288
|
+
public deleteItem(item: KeysType) {
|
|
289
|
+
return deleteItem(this.tableName, item.partitionKey, item.rowKey);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function isError(raw: FullOperationResponse) {
|
|
294
|
+
let isError = false;
|
|
295
|
+
let message: string = null;
|
|
296
|
+
|
|
297
|
+
//201 - table created successfully
|
|
298
|
+
//204 - entity created successfully
|
|
299
|
+
//409 - table/entity already exists
|
|
300
|
+
//400 - table name not allowed
|
|
301
|
+
//404 - TableNotFound when adding item
|
|
302
|
+
if (raw.status >= 400) {
|
|
303
|
+
isError = true;
|
|
304
|
+
let error = raw.parsedBody as IODataError;
|
|
305
|
+
message = error && error.odataError && !isNullOrEmptyString(error.odataError.code)
|
|
306
|
+
? error.odataError.code
|
|
307
|
+
: `Unknown error`;//for some errors like table name too short, error code is empty
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
isError = false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { isError, message };
|
|
314
|
+
}
|