Skip to content

Commit

Permalink
feat: create an lightweight ORM for saving (#3759)
Browse files Browse the repository at this point in the history
* feat: create an lightweight ORM for saving

* add rawget/rawset to lua global

* remove previously used enums

* chaining

* annotations and do not run strip for extradata
  • Loading branch information
Rathoz authored Jan 11, 2024
1 parent f3cce73 commit c8b770f
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ std = {
"package",
"pairs",
"pcall",
"rawget",
"rawset",
"require",
"select",
"setmetatable",
Expand Down
73 changes: 73 additions & 0 deletions spec/orm_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
--- Triple Comment to Enable our LLS Plugin
describe('LPDB Object-Relational Mapping', function()
local Lpdb = require('Module:Lpdb')

describe('setting data', function()
it('assign value on init', function()
local match2 = Lpdb.Match2:new({bestof = 10})
assert.are_same({bestof = 10}, rawget(match2, 'fields'))
end)

it('__newindex', function()
local match2 = Lpdb.Match2:new()
match2.bestof = 7
assert.are_same({bestof = 7}, rawget(match2, 'fields'))
end)

it('set()', function()
local match2 = Lpdb.Match2:new():set('bestof', 5)
assert.are_same({bestof = 5}, rawget(match2, 'fields'))
end)

it('setMany()', function()
local match2 = Lpdb.Match2:new():setMany{bestof = 3, game = 'r6s'}
assert.are_same({bestof = 3, game = 'r6s'}, rawget(match2, 'fields'))
end)

it('strip html tags', function()
local match2 = Lpdb.Match2:new():set('match2bracketdata', {header = '<abbr title="Best of 5">Bo5</abbr>'})
assert.are_same({match2bracketdata = {header = 'Bo5'}}, rawget(match2, 'fields'))
end)
end)

describe('saving data', function()
it('saving', function()
local stub = stub(mw.ext.LiquipediaDB, 'lpdb_match2')
Lpdb.Match2:new({match2id = 'Foo', match2bracketid = 'Bar', bestof = 3, game = 'r6s'}):save()
assert.stub(stub).called_with('Foo', {
bestof = 3,
date = 0,
dateexact = 0,
extradata = {},
finished = 0,
game = 'r6s',
icon = '',
icondark = '',
links = {},
liquipediatier = '',
liquipediatiertype = '',
match2bracketdata = {},
match2bracketid = 'Bar',
match2games = {},
match2id = 'Foo',
match2opponents = {},
mode = '',
parent = '',
patch = '',
publishertier = '',
resulttype = '',
section = '',
series = '',
shortname = '',
stream = {},
tickername = '',
tournament = '',
type = '',
vod = '',
walkover = '',
winner = '',
})
stub:revert()
end)
end)
end)
160 changes: 158 additions & 2 deletions standard/lpdb.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@
-- Please see https://github.com/Liquipedia/Lua-Modules to contribute
--

local Array = require('Module:Array')
local Class = require('Module:Class')
local FnUtil = require('Module:FnUtil')
local Table = require('Module:Table')
local TextSanitizer = require('Module:TextSanitizer')

local Lpdb = {}

local _MAXIMUM_QUERY_LIMIT = 5000
local MAXIMUM_QUERY_LIMIT = 5000

-- Executes a mass query.
--[==[
Expand All @@ -19,6 +25,7 @@ Loops LPDB queries to e.g.
or additional limitations are reached
example:
```
local foundMatchIds = {}
local getMatchId = function(match)
if #foundMatchIds < args.matchLimit then
Expand All @@ -41,14 +48,15 @@ example:
Lpdb.executeMassQuery('match2', queryParameters, getMatchId)
return foundMatchIds
```
]==]
---@param tableName string
---@param queryParameters table
---@param itemChecker fun(item: table): boolean?
---@param limit number?
function Lpdb.executeMassQuery(tableName, queryParameters, itemChecker, limit)
queryParameters.offset = queryParameters.offset or 0
queryParameters.limit = queryParameters.limit or _MAXIMUM_QUERY_LIMIT
queryParameters.limit = queryParameters.limit or MAXIMUM_QUERY_LIMIT
limit = limit or math.huge

while queryParameters.offset < limit do
Expand All @@ -69,4 +77,152 @@ function Lpdb.executeMassQuery(tableName, queryParameters, itemChecker, limit)
end
end

--- LPDB Object-Relational Mapping

---@alias ModelColumnData {name: string, fieldType: string|any, default: any}

---@class Model
---@field tableName string
---@field tableColumns ModelColumnData[]
local Model = Class.new(function(self, name, columns)
self.tableName = name
self.tableColumns = columns
end)

---@class ModelRow
---@field private tableName string
---@field private tableColumns ModelColumnData[]
---@field private fields table<string, any>
---@field [any] any
local ModelRow = Class.new(function(self, tableName, tableColumns)
rawset(self, 'tableName', tableName)
rawset(self, 'tableColumns', tableColumns)
rawset(self, 'fields', {})
end)

---@param initData table?
---@return ModelRow
function Model:new(initData)
local row = ModelRow(self.tableName, self.tableColumns)
if type(initData) == 'table' then
row:setMany(initData)
end
return row
end

---@private
---@param columnData ModelColumnData
function ModelRow:_validateField(columnData)
if not self.fields[columnData.name] then
error(self.tableName .. ' expects ' .. columnData.name .. ' to be set')
end
-- TODO: Verify types (at least when running tests)
end

---@private
---@param columnData ModelColumnData
function ModelRow:_prepareFieldForStorage(columnData)
-- Apply defaults
if not self.fields[columnData.name] then
if type(columnData.default) == 'function' then
self.fields[columnData.name] = columnData.default(self.fields)
else
self.fields[columnData.name] = columnData.default
end
end

-- Validate that all fields are correct
self:_validateField(columnData)
end

---@return self
function ModelRow:save()
Array.forEach(self.tableColumns, FnUtil.curry(ModelRow._prepareFieldForStorage, self))
local objectName = Table.extract(self.fields, 'objectname')
mw.ext.LiquipediaDB['lpdb_' .. self.tableName](objectName, self.fields)
return self
end

---@param key string
---@param value any
---@return self
function ModelRow:__newindex(key, value)
if key ~= 'extradata' then
-- Strip HTML from strings
-- We allow HTML in extradata though
local function stripHtml(str)
if type(str) == 'string' then
return TextSanitizer.stripHTML(str)
end
return str
end

if type(value) == 'table' then
value = Table.mapValues(value, stripHtml)
else
value = stripHtml(value)
end
end

self.fields[key] = value
return self
end

---@param key string
---@param value any
---@return self
function ModelRow:set(key, value)
self:__newindex(key, value)
return self
end

---@param tbl table<string, any>
---@return self
function ModelRow:setMany(tbl)
Table.iter.forEachPair(tbl, FnUtil.curry(ModelRow.__newindex, self))
return self
end

---@class Match2Model:Model
Lpdb.Match2 = Model('match2', {
{
name = 'objectname',
fieldType = 'string',
default = function(fields)
return fields.match2id
end
},
{name = 'match2id', fieldType = 'string'},
{name = 'match2bracketid', fieldType = 'struct'},
{name = 'winner', fieldType = 'string', default = ''},
{name = 'walkover', fieldType = 'string', default = ''},
{name = 'resulttype', fieldType = 'string', default = ''},
{name = 'finished', fieldType = 'number', default = 0},
{name = 'mode', fieldType = 'string', default = ''},
{name = 'type', fieldType = 'string', default = ''},
{name = 'section', fieldType = 'string', default = ''},
{name = 'game', fieldType = 'string', default = ''},
{name = 'patch', fieldType = 'string', default = ''},
{name = 'date', fieldType = 'string', default = 0},
{name = 'dateexact', fieldType = 'number', default = 0},
{name = 'stream', fieldType = 'struct', default = {}},
{name = 'links', fieldType = 'struct', default = {}},
{name = 'bestof', fieldType = 'number', default = 0},
{name = 'vod', fieldType = 'string', default = ''},
{name = 'tournament', fieldType = 'string', default = ''},
{name = 'parent', fieldType = 'pagename', default = ''},
{name = 'tickername', fieldType = 'string', default = ''},
{name = 'shortname', fieldType = 'string', default = ''},
{name = 'series', fieldType = 'string', default = ''},
{name = 'icon', fieldType = 'string', default = ''},
{name = 'icondark', fieldType = 'string', default = ''},
{name = 'liquipediatier', fieldType = 'string|number', default = ''},
{name = 'liquipediatiertype', fieldType = 'string', default = ''},
{name = 'publishertier', fieldType = 'string', default = ''},
{name = 'extradata', fieldType = 'struct', default = {}},
{name = 'match2bracketdata', fieldType = 'struct', default = {}},
{name = 'match2opponents', fieldType = 'array', default = {}},
{name = 'match2games', fieldType = 'array', default = {}},
})

return Lpdb

0 comments on commit c8b770f

Please sign in to comment.