forked from makeomatic/ms-users
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathupdateMetadata.lua
More file actions
252 lines (211 loc) · 6.7 KB
/
Copy pathupdateMetadata.lua
File metadata and controls
252 lines (211 loc) · 6.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
-- Script performs User/Organization metadata update and tracks used audiences
-- KEYS[1] = Audience Key template in format `{id}someExtraText` - Key stores currently used audiences associated with metadata
-- KEYS[2] = Metadata Key template in format `{id}myAvesomeMEtaKey{audience}` - Key stores metadata
-- `{id}` and `{audience}` will be replaced with real values on script runtime
-- ARGV[1] = Id of the User/Organization which is going to be updated
-- ARGV[2] = JsonString with list of operations to execute on the metadata of the provided Id
-- script replicates commands instead of own body
-- call of HMSET command is determined as 'non deterministic command'
-- and redis refuses to run it without this.
redis.replicate_commands()
local audienceKeyTemplate = KEYS[1]
local metaDataTemplate = KEYS[2]
local Id = ARGV[1]
local updateOptsJson = ARGV[2]
local scriptResult = { err = nil, ok = {}}
--
-- Param Validation
--
local function isValidString(val)
if type(val) == 'string' and string.len(val) > 0 then
return true
end
return false
end
assert(isValidString(Id), 'incorrect `id` argument')
assert(isValidString(updateOptsJson), 'incorrect `updateJson` argument')
local updateOpts = cjson.decode(updateOptsJson)
--
-- Internal functions
--
-- evaluates provided script
local function evalLuaScript(code, environment)
if setfenv and loadstring then
local f = assert(loadstring(code))
setfenv(f, environment)
return f
else
return assert(load(code, nil, "t", environment))
end
end
-- creates array with unique items from passed arrays
local function getUniqueItemsFromTables(...)
local args = {...}
local tableWithUniqueItems = {}
for _, passedTable in pairs(args) do
for __, keyName in pairs(passedTable) do
tableWithUniqueItems[keyName] = keyName
end
end
return tableWithUniqueItems
end
-- create key from passed template, id and audience
local function makeRedisKey (template, id, audience)
local str = template:gsub('{id}', id, 1)
if audience ~= nil then
str = str:gsub('{audience}', audience, 1)
end
return str
end
local function getResultOrSaveError(result, command, args)
if type(result) == 'table' and result['err'] ~= nil then
if (scriptResult['err'] == nil) then
scriptResult['err'] = {}
end
table.insert(scriptResult['err'], {
err = result['err'],
command = {
name = command,
args = args
}
})
return nil
end
return result
end
--
-- available Meta Operations definition
--
-- $set: { field: value, field2: value, field3: value }
-- { HMSETResponse }
local function opSet(metaKey, args)
local setArgs = {}
for field, value in pairs(args) do
table.insert(setArgs, field)
table.insert(setArgs, value)
end
if #setArgs < 1 then
return nil
end
local cmdResult = redis.pcall("HMSET", metaKey, unpack(setArgs))
cmdResult = getResultOrSaveError(cmdResult, "HMSET", setArgs)
if cmdResult ~= nil then
return cmdResult.ok
end
return cmdResult
end
-- $remove: [ 'field', 'field2' ]
-- { deletedFieldsCount } - if no fields deleted or there was no such fields counter not incrementing
local function opRemove(metaKey, args)
local result = 0;
for _, field in pairs(args) do
local cmdResult = redis.pcall("HDEL", metaKey, field)
result = result + getResultOrSaveError(cmdResult, "HDEL", { metaKey, field })
end
return result
end
-- $incr: { field: incrValue, field2: incrValue }
-- { field: newValue }
local function opIncr(metaKey, args)
local result = {}
for field, incrVal in pairs(args) do
-- TODO fix err
local cmdResult = redis.pcall("HINCRBY", metaKey, field, incrVal)
cmdResult = getResultOrSaveError(cmdResult, "HINCRBY", { metaKey, field, incrVal })
result[field] = cmdResult
end
-- if #result > 0 then
-- return result
-- end
--
-- return nil
return result;
end
-- operations index
local metaOps = {
['$set'] = opSet,
['$remove'] = opRemove,
['$incr'] = opIncr
}
--
-- Script body
--
-- get list of keys to update
-- generate them from passed audiences and metaData key template
local keysToProcess = {};
for index, audience in ipairs(updateOpts.audiences) do
local key = makeRedisKey(metaDataTemplate, Id, audience)
table.insert(keysToProcess, index, key);
end
-- process meta update operations
if updateOpts.metaOps then
-- iterate over metadata hash field
for index, op in ipairs(updateOpts.metaOps) do
local targetOpKey = keysToProcess[index]
local metaProcessResult = {};
-- iterate over commands and apply them
for opName, opArg in pairs(op) do
local processFn = metaOps[opName];
if processFn ~= nil then
-- store command execution result
metaProcessResult[opName] = processFn(targetOpKey, opArg)
end
end
-- store execution result of commands block
table.insert(scriptResult['ok'], metaProcessResult)
end
-- process passed scripts
elseif updateOpts.scripts then
-- iterate over scripts and execute them in sandbox
for _, script in pairs(updateOpts.scripts) do
local env = {};
-- allow read access to this script scope
-- env recreated for each script to avoid scope mixing
setmetatable(env, { __index=_G })
-- override params to be sure that script works like it was executed like from `redis.eval` command
env.ARGV = script.argv
env.KEYS = keysToProcess
-- evaluate script and bind to custom env
local fn = evalLuaScript(script.lua, env)
-- run script and save result
local status, result = pcall(fn)
if status == true then
scriptResult['ok'][script.name] = result;
else
if (scriptResult['err'] == nil) then
scriptResult['err'] = {}
end
table.insert(scriptResult['err'], {
err = result,
script = script.name,
keys = keysToProcess,
args = script.args,
})
end
end
end
--
-- Audience tracking
--
local audienceKey = makeRedisKey(audienceKeyTemplate, Id)
-- get saved audience list
local audiences = redis.call("SMEMBERS", audienceKey)
-- create list containing saved and possibly new audiences
local uniqueAudiences = getUniqueItemsFromTables(audiences, updateOpts.audiences)
-- iterate over final audience list
for _, audience in pairs(uniqueAudiences) do
-- get size of metaKey
local metaKey = makeRedisKey(metaDataTemplate, Id, audience)
local keyLen = redis.call("HLEN", metaKey)
-- if key has data add it to the audience set
-- set members unique, so duplicates not appear
-- if key empty or not exists (HLEN will return 0)
-- delete audience from list
if (keyLen > 0) then
redis.call("SADD", audienceKey, audience)
else
redis.call("SREM", audienceKey, audience)
end
end
-- respond with json encoded string
return cjson.encode(scriptResult)