mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) updates from grist-core
This commit is contained in:
commit
c02acff361
@ -6,7 +6,7 @@ root = true
|
|||||||
# Unix-style newlines with a newline ending every file
|
# Unix-style newlines with a newline ending every file
|
||||||
[*.{ts,js}]
|
[*.{ts,js}]
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = false
|
insert_final_newline = true
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
# indent with 2 spaces
|
# indent with 2 spaces
|
||||||
indent_style = space
|
indent_style = space
|
||||||
|
@ -58,6 +58,7 @@ export interface AclMatchInput {
|
|||||||
user: UserInfo;
|
user: UserInfo;
|
||||||
rec?: InfoView;
|
rec?: InfoView;
|
||||||
newRec?: InfoView;
|
newRec?: InfoView;
|
||||||
|
docId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -200,17 +200,20 @@ export async function handleSandboxErrorOnPlatform<T>(
|
|||||||
platform.throwError('', `Invalid row id ${match[1]}`, 400);
|
platform.throwError('', `Invalid row id ${match[1]}`, 400);
|
||||||
}
|
}
|
||||||
match = message.match(
|
match = message.match(
|
||||||
/\[Sandbox] (?:KeyError u?'(?:Table \w+ has no column )?|ValueError No such table: )(\w+)/
|
// eslint-disable-next-line max-len
|
||||||
|
/\[Sandbox] (?:KeyError u?'(?:Table \w+ has no column )?|ValueError No such table: |ValueError No such column: )([\w.]+)/
|
||||||
);
|
);
|
||||||
if (match) {
|
if (match) {
|
||||||
if (match[1] === tableId) {
|
if (match[1] === tableId) {
|
||||||
platform.throwError('', `Table not found "${tableId}"`, 404);
|
platform.throwError('', `Table not found "${tableId}"`, 404);
|
||||||
} else if (colNames.includes(match[1])) {
|
} else if (colNames.includes(match[1])) {
|
||||||
platform.throwError('', `Invalid column "${match[1]}"`, 400);
|
platform.throwError('', `Invalid column "${match[1]}"`, 400);
|
||||||
|
} else if (colNames.includes(match[1].replace(`${tableId}.`, ''))) {
|
||||||
|
platform.throwError('', `Table or column not found "${match[1]}"`, 404);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
platform.throwError('', `Error manipulating data: ${message}`, 400);
|
platform.throwError('', `Error manipulating data: ${message}`, 400);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -55,8 +55,8 @@ function _compileNode(parsedAclFormula: ParsedAclFormula): AclEvalFunc {
|
|||||||
case 'GtE': return _compileAndCombine(args, ([a, b]) => a >= b);
|
case 'GtE': return _compileAndCombine(args, ([a, b]) => a >= b);
|
||||||
case 'Is': return _compileAndCombine(args, ([a, b]) => a === b);
|
case 'Is': return _compileAndCombine(args, ([a, b]) => a === b);
|
||||||
case 'IsNot': return _compileAndCombine(args, ([a, b]) => a !== b);
|
case 'IsNot': return _compileAndCombine(args, ([a, b]) => a !== b);
|
||||||
case 'In': return _compileAndCombine(args, ([a, b]) => b.includes(a));
|
case 'In': return _compileAndCombine(args, ([a, b]) => Boolean(b?.includes(a)));
|
||||||
case 'NotIn': return _compileAndCombine(args, ([a, b]) => !b.includes(a));
|
case 'NotIn': return _compileAndCombine(args, ([a, b]) => !b?.includes(a));
|
||||||
case 'List': return _compileAndCombine(args, (values) => values);
|
case 'List': return _compileAndCombine(args, (values) => values);
|
||||||
case 'Const': return constant(parsedAclFormula[1] as CellValue);
|
case 'Const': return constant(parsedAclFormula[1] as CellValue);
|
||||||
case 'Name': {
|
case 'Name': {
|
||||||
|
@ -746,6 +746,17 @@ export class DocWorkerApi {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this._app.delete('/api/docs/:docId/tables/:tableId/columns/:colId', canEdit,
|
||||||
|
withDoc(async (activeDoc, req, res) => {
|
||||||
|
const {tableId, colId} = req.params;
|
||||||
|
const actions = [ [ 'RemoveColumn', tableId, colId ] ];
|
||||||
|
await handleSandboxError(tableId, [colId],
|
||||||
|
activeDoc.applyUserActions(docSessionFromRequest(req), actions)
|
||||||
|
);
|
||||||
|
res.json(null);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Add a new webhook and trigger
|
// Add a new webhook and trigger
|
||||||
this._app.post('/api/docs/:docId/webhooks', isOwner, validate(WebhookSubscribeCollection),
|
this._app.post('/api/docs/:docId/webhooks', isOwner, validate(WebhookSubscribeCollection),
|
||||||
withDoc(async (activeDoc, req, res) => {
|
withDoc(async (activeDoc, req, res) => {
|
||||||
|
@ -342,6 +342,7 @@ export class GranularAccess implements GranularAccessForBundle {
|
|||||||
public async inputs(docSession: OptDocSession): Promise<AclMatchInput> {
|
public async inputs(docSession: OptDocSession): Promise<AclMatchInput> {
|
||||||
return {
|
return {
|
||||||
user: await this._getUser(docSession),
|
user: await this._getUser(docSession),
|
||||||
|
docId: this._docId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +219,8 @@ function evaluateRule(ruleSet: RuleSet, input: AclMatchInput): PartialPermission
|
|||||||
// Anything it would explicitly allow, no longer allow through this rule.
|
// Anything it would explicitly allow, no longer allow through this rule.
|
||||||
// Anything it would explicitly deny, go ahead and deny.
|
// Anything it would explicitly deny, go ahead and deny.
|
||||||
pset = mergePartialPermissions(pset, mapValues(rule.permissions, val => (val === 'allow' ? "" : val)));
|
pset = mergePartialPermissions(pset, mapValues(rule.permissions, val => (val === 'allow' ? "" : val)));
|
||||||
log.warn("ACLRule for %s failed: %s", ruleSet.tableId, e.message);
|
const prefixedTableName = input.docId ? `${input.docId}.${ruleSet.tableId}` : ruleSet.tableId;
|
||||||
|
log.warn("ACLRule for %s (`%s`) failed: %s", prefixedTableName, rule.aclFormula, e.stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -224,3 +224,14 @@ export function sanitizeWorksheetName(tableName: string): string {
|
|||||||
.replace(/^['\s]+/, '')
|
.replace(/^['\s]+/, '')
|
||||||
.replace(/['\s]+$/, '');
|
.replace(/['\s]+$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This method exists only to make Piscina happier. With it,
|
||||||
|
// Piscina will load this file using a regular require(),
|
||||||
|
// which under Electron will deal fine with Electron's ASAR
|
||||||
|
// app bundle. Without it, Piscina will try fancier methods
|
||||||
|
// that aren't at the time of writing correctly patched to
|
||||||
|
// deal with an ASAR app bundle, and so report that this
|
||||||
|
// file doesn't exist instead of exporting an XLSX file.
|
||||||
|
// https://github.com/gristlabs/grist-electron/issues/9
|
||||||
|
export default function doNothing() {
|
||||||
|
}
|
||||||
|
@ -747,7 +747,9 @@
|
|||||||
"Support Grist": "Suporte Grist",
|
"Support Grist": "Suporte Grist",
|
||||||
"Activation": "Ativação",
|
"Activation": "Ativação",
|
||||||
"Billing Account": "Conta de faturação",
|
"Billing Account": "Conta de faturação",
|
||||||
"Upgrade Plan": "Atualizar o Plano"
|
"Upgrade Plan": "Atualizar o Plano",
|
||||||
|
"Sign In": "Entrar",
|
||||||
|
"Sign Up": "Inscrever-se"
|
||||||
},
|
},
|
||||||
"ViewAsDropdown": {
|
"ViewAsDropdown": {
|
||||||
"View As": "Ver como",
|
"View As": "Ver como",
|
||||||
|
@ -13,7 +13,7 @@ describe('ACLFormula', function() {
|
|||||||
// Turn off logging for this test, and restore afterwards.
|
// Turn off logging for this test, and restore afterwards.
|
||||||
testUtils.setTmpLogLevel('error');
|
testUtils.setTmpLogLevel('error');
|
||||||
|
|
||||||
const docTools = createDocTools();
|
const docTools = createDocTools({ persistAcrossCases: true });
|
||||||
const fakeSession = makeExceptionalDocSession('system');
|
const fakeSession = makeExceptionalDocSession('system');
|
||||||
|
|
||||||
function getInfoView(row: Record<string, CellValue>): InfoView {
|
function getInfoView(row: Record<string, CellValue>): InfoView {
|
||||||
@ -25,47 +25,72 @@ describe('ACLFormula', function() {
|
|||||||
|
|
||||||
const V = getInfoView; // A shortcut.
|
const V = getInfoView; // A shortcut.
|
||||||
|
|
||||||
it('should correctly interpret parsed ACL formulas', async function() {
|
type SetAndCompile = (aclFormula: string) => Promise<AclMatchFunc>;
|
||||||
|
let setAndCompile: SetAndCompile;
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
const docName = 'docdata1';
|
const docName = 'docdata1';
|
||||||
const activeDoc1 = await docTools.createDoc(docName);
|
const activeDoc1 = await docTools.createDoc(docName);
|
||||||
let compiled: AclMatchFunc;
|
|
||||||
|
|
||||||
const resourceRef = (await activeDoc1.applyUserActions(fakeSession,
|
const resourceRef = (await activeDoc1.applyUserActions(fakeSession,
|
||||||
[['AddRecord', '_grist_ACLResources', null, {tableId: '*', colIds: '*'}]])).retValues[0];
|
[['AddRecord', '_grist_ACLResources', null, {tableId: '*', colIds: '*'}]])).retValues[0];
|
||||||
const ruleRef = (await activeDoc1.applyUserActions(fakeSession,
|
const ruleRef = (await activeDoc1.applyUserActions(fakeSession,
|
||||||
[['AddRecord', '_grist_ACLRules', null, {resource: resourceRef}]])).retValues[0];
|
[['AddRecord', '_grist_ACLRules', null, {resource: resourceRef}]])).retValues[0];
|
||||||
|
|
||||||
async function setAndCompile(aclFormula: string): Promise<AclMatchFunc> {
|
setAndCompile = async function setAndCompile(aclFormula) {
|
||||||
await activeDoc1.applyUserActions(fakeSession, [['UpdateRecord', '_grist_ACLRules', ruleRef, {aclFormula}]]);
|
await activeDoc1.applyUserActions(fakeSession, [['UpdateRecord', '_grist_ACLRules', ruleRef, {aclFormula}]]);
|
||||||
const {tableData} = await activeDoc1.fetchQuery(
|
const {tableData} = await activeDoc1.fetchQuery(
|
||||||
fakeSession, {tableId: '_grist_ACLRules', filters: {id: [ruleRef]}});
|
fakeSession, {tableId: '_grist_ACLRules', filters: {id: [ruleRef]}});
|
||||||
assert(tableData[3].aclFormulaParsed, "Expected aclFormulaParsed to be populated");
|
assert(tableData[3].aclFormulaParsed, "Expected aclFormulaParsed to be populated");
|
||||||
const parsedFormula = String(tableData[3].aclFormulaParsed[0]);
|
const parsedFormula = String(tableData[3].aclFormulaParsed[0]);
|
||||||
return compileAclFormula(JSON.parse(parsedFormula));
|
return compileAclFormula(JSON.parse(parsedFormula));
|
||||||
}
|
};
|
||||||
|
});
|
||||||
|
|
||||||
compiled = await setAndCompile("user.Email == 'X@'");
|
it('should handle a comparison', async function() {
|
||||||
|
const compiled = await setAndCompile("user.Email == 'X@'");
|
||||||
assert.equal(compiled({user: new User({Email: 'X@'})}), true);
|
assert.equal(compiled({user: new User({Email: 'X@'})}), true);
|
||||||
assert.equal(compiled({user: new User({Email: 'Y@'})}), false);
|
assert.equal(compiled({user: new User({Email: 'Y@'})}), false);
|
||||||
assert.equal(compiled({user: new User({Email: 'X'}), rec: V({Email: 'Y@'})}), false);
|
assert.equal(compiled({user: new User({Email: 'X'}), rec: V({Email: 'Y@'})}), false);
|
||||||
assert.equal(compiled({user: new User({Name: 'X@'})}), false);
|
assert.equal(compiled({user: new User({Name: 'X@'})}), false);
|
||||||
|
});
|
||||||
|
|
||||||
compiled = await setAndCompile("user.Role in ('editors', 'owners')");
|
it('should handle the "in" operator', async function () {
|
||||||
|
const compiled = await setAndCompile("user.Role in ('editors', 'owners')");
|
||||||
assert.equal(compiled({user: new User({Role: 'editors'})}), true);
|
assert.equal(compiled({user: new User({Role: 'editors'})}), true);
|
||||||
assert.equal(compiled({user: new User({Role: 'owners'})}), true);
|
assert.equal(compiled({user: new User({Role: 'owners'})}), true);
|
||||||
assert.equal(compiled({user: new User({Role: 'viewers'})}), false);
|
assert.equal(compiled({user: new User({Role: 'viewers'})}), false);
|
||||||
assert.equal(compiled({user: new User({Role: null})}), false);
|
assert.equal(compiled({user: new User({Role: null})}), false);
|
||||||
assert.equal(compiled({user: new User({})}), false);
|
assert.equal(compiled({user: new User({})}), false);
|
||||||
|
});
|
||||||
|
|
||||||
// Opposite of the previous formula.
|
it('should handle the "not in" operator', async function () {
|
||||||
compiled = await setAndCompile("user.Role not in ('editors', 'owners')");
|
const compiled = await setAndCompile("user.Role not in ('editors', 'owners')");
|
||||||
assert.equal(compiled({user: new User({Role: 'editors'})}), false);
|
assert.equal(compiled({user: new User({Role: 'editors'})}), false);
|
||||||
assert.equal(compiled({user: new User({Role: 'owners'})}), false);
|
assert.equal(compiled({user: new User({Role: 'owners'})}), false);
|
||||||
assert.equal(compiled({user: new User({Role: 'viewers'})}), true);
|
assert.equal(compiled({user: new User({Role: 'viewers'})}), true);
|
||||||
assert.equal(compiled({user: new User({Role: null})}), true);
|
assert.equal(compiled({user: new User({Role: null})}), true);
|
||||||
assert.equal(compiled({user: new User({})}), true);
|
assert.equal(compiled({user: new User({})}), true);
|
||||||
|
});
|
||||||
|
|
||||||
compiled = await setAndCompile("rec.office == 'Seattle' and user.email in ['sally@', 'xie@']");
|
[{
|
||||||
|
op: 'in'
|
||||||
|
}, {
|
||||||
|
op: 'not in'
|
||||||
|
}].forEach(ctx => {
|
||||||
|
it(`should handle the "${ctx.op}" operator with a string RHS to check if substring exist`, async function() {
|
||||||
|
const compiled = await setAndCompile(`user.Name ${ctx.op} 'FooBar'`);
|
||||||
|
assert.equal(compiled({user: new User({Name: 'FooBar'})}), ctx.op === 'in');
|
||||||
|
assert.equal(compiled({user: new User({Name: 'Foo'})}), ctx.op === 'in');
|
||||||
|
assert.equal(compiled({user: new User({Name: 'Bar'})}), ctx.op === 'in');
|
||||||
|
assert.equal(compiled({user: new User({Name: 'bar'})}), ctx.op === 'not in');
|
||||||
|
assert.equal(compiled({user: new User({Name: 'qux'})}), ctx.op === 'not in');
|
||||||
|
assert.equal(compiled({user: new User({Name: null})}), ctx.op === 'not in');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle the "and" operator', async function () {
|
||||||
|
const compiled = await setAndCompile("rec.office == 'Seattle' and user.email in ['sally@', 'xie@']");
|
||||||
assert.throws(() => compiled({user: new User({email: 'xie@'})}), /Missing row data 'rec'/);
|
assert.throws(() => compiled({user: new User({email: 'xie@'})}), /Missing row data 'rec'/);
|
||||||
assert.equal(compiled({user: new User({email: 'xie@'}), rec: V({})}), false);
|
assert.equal(compiled({user: new User({email: 'xie@'}), rec: V({})}), false);
|
||||||
assert.equal(compiled({user: new User({email: 'xie@'}), rec: V({office: null})}), false);
|
assert.equal(compiled({user: new User({email: 'xie@'}), rec: V({office: null})}), false);
|
||||||
@ -75,9 +100,19 @@ describe('ACLFormula', function() {
|
|||||||
assert.equal(compiled({user: new User({email: 'sally@'}), rec: V({office: 'Chicago'})}), false);
|
assert.equal(compiled({user: new User({email: 'sally@'}), rec: V({office: 'Chicago'})}), false);
|
||||||
assert.equal(compiled({user: new User({email: null}), rec: V({office: null})}), false);
|
assert.equal(compiled({user: new User({email: null}), rec: V({office: null})}), false);
|
||||||
assert.equal(compiled({user: new User({}), rec: V({})}), false);
|
assert.equal(compiled({user: new User({}), rec: V({})}), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle the "or" operator', async function () {
|
||||||
|
const compiled = await setAndCompile('user.Email=="X@" or user.Email is None');
|
||||||
|
assert.equal(compiled({user: new User({Email: 'X@'})}), true);
|
||||||
|
assert.equal(compiled({user: new User({})}), true);
|
||||||
|
assert.equal(compiled({user: new User({Email: 'Y@'})}), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a complex combination of operators', async function () {
|
||||||
|
|
||||||
// This is not particularly meaningful, but involves more combinations.
|
// This is not particularly meaningful, but involves more combinations.
|
||||||
compiled = await setAndCompile(
|
const compiled = await setAndCompile(
|
||||||
"user.IsAdmin or rec.assigned is None or (not newRec.HasDuplicates and rec.StatusIndex <= newRec.StatusIndex)");
|
"user.IsAdmin or rec.assigned is None or (not newRec.HasDuplicates and rec.StatusIndex <= newRec.StatusIndex)");
|
||||||
assert.equal(compiled({user: new User({IsAdmin: true})}), true);
|
assert.equal(compiled({user: new User({IsAdmin: true})}), true);
|
||||||
assert.equal(compiled({user: new User({IsAdmin: 17})}), true);
|
assert.equal(compiled({user: new User({IsAdmin: 17})}), true);
|
||||||
@ -102,9 +137,10 @@ describe('ACLFormula', function() {
|
|||||||
newRec: V({HasDuplicates: false, StatusIndex: 1})}), false);
|
newRec: V({HasDuplicates: false, StatusIndex: 1})}), false);
|
||||||
assert.equal(compiled({user: new User({IsAdmin: false}), rec: V({assigned: 1, StatusIndex: 1}),
|
assert.equal(compiled({user: new User({IsAdmin: false}), rec: V({assigned: 1, StatusIndex: 1}),
|
||||||
newRec: V({HasDuplicates: true, StatusIndex: 17})}), false);
|
newRec: V({HasDuplicates: true, StatusIndex: 17})}), false);
|
||||||
|
});
|
||||||
|
|
||||||
// Test arithmetic.
|
it('should handle arithmetic tests', async function () {
|
||||||
compiled = await setAndCompile(
|
const compiled = await setAndCompile(
|
||||||
"rec.A <= rec.B + 1 and rec.A >= rec.B - 1 and rec.A < rec.C * 2.5 and rec.A > rec.C / 2.5 and rec.A % 2 != 0");
|
"rec.A <= rec.B + 1 and rec.A >= rec.B - 1 and rec.A < rec.C * 2.5 and rec.A > rec.C / 2.5 and rec.A % 2 != 0");
|
||||||
assert.equal(compiled({user: new User({}), rec: V({A: 3, B: 3, C: 3})}), true);
|
assert.equal(compiled({user: new User({}), rec: V({A: 3, B: 3, C: 3})}), true);
|
||||||
assert.equal(compiled({user: new User({}), rec: V({A: 3, B: 4, C: 3})}), true);
|
assert.equal(compiled({user: new User({}), rec: V({A: 3, B: 4, C: 3})}), true);
|
||||||
@ -117,9 +153,10 @@ describe('ACLFormula', function() {
|
|||||||
assert.equal(compiled({user: new User({}), rec: V({A: 1.3, B: 1, C: 3})}), true);
|
assert.equal(compiled({user: new User({}), rec: V({A: 1.3, B: 1, C: 3})}), true);
|
||||||
assert.equal(compiled({user: new User({}), rec: V({A: 7.4, B: 7, C: 3})}), true);
|
assert.equal(compiled({user: new User({}), rec: V({A: 7.4, B: 7, C: 3})}), true);
|
||||||
assert.equal(compiled({user: new User({}), rec: V({A: 7.5, B: 7, C: 3})}), false);
|
assert.equal(compiled({user: new User({}), rec: V({A: 7.5, B: 7, C: 3})}), false);
|
||||||
|
});
|
||||||
|
|
||||||
// Test is/is-not.
|
it('should handle "is" and "is not" operators', async function () {
|
||||||
compiled = await setAndCompile(
|
const compiled = await setAndCompile(
|
||||||
"rec.A is True or rec.B is not False");
|
"rec.A is True or rec.B is not False");
|
||||||
assert.equal(compiled({user: new User({}), rec: V({A: true})}), true);
|
assert.equal(compiled({user: new User({}), rec: V({A: true})}), true);
|
||||||
assert.equal(compiled({user: new User({}), rec: V({A: 2})}), true);
|
assert.equal(compiled({user: new User({}), rec: V({A: 2})}), true);
|
||||||
@ -130,9 +167,10 @@ describe('ACLFormula', function() {
|
|||||||
assert.equal(compiled({user: new User({}), rec: V({A: null, B: 2})}), true);
|
assert.equal(compiled({user: new User({}), rec: V({A: null, B: 2})}), true);
|
||||||
assert.equal(compiled({user: new User({}), rec: V({A: null, B: false})}), false);
|
assert.equal(compiled({user: new User({}), rec: V({A: null, B: false})}), false);
|
||||||
assert.equal(compiled({user: new User({}), rec: V({A: null, B: 0})}), true);
|
assert.equal(compiled({user: new User({}), rec: V({A: null, B: 0})}), true);
|
||||||
|
});
|
||||||
|
|
||||||
// Test nested attribute lookups.
|
it('should handle nested attribute lookups', async function () {
|
||||||
compiled = await setAndCompile('user.office.city == "New York"');
|
const compiled = await setAndCompile('user.office.city == "New York"');
|
||||||
assert.equal(compiled({user: new User({office: V({city: "New York"})})}), true);
|
assert.equal(compiled({user: new User({office: V({city: "New York"})})}), true);
|
||||||
assert.equal(compiled({user: new User({office: V({city: "Boston"})})}), false);
|
assert.equal(compiled({user: new User({office: V({city: "Boston"})})}), false);
|
||||||
assert.equal(compiled({user: new User({office: V({city: null})})}), false);
|
assert.equal(compiled({user: new User({office: V({city: null})})}), false);
|
||||||
@ -140,4 +178,19 @@ describe('ACLFormula', function() {
|
|||||||
assert.equal(compiled({user: new User({office: 5})}), false);
|
assert.equal(compiled({user: new User({office: 5})}), false);
|
||||||
assert.throws(() => compiled({user: new User({office: null})}), /No value for 'user.office'/);
|
assert.throws(() => compiled({user: new User({office: null})}), /No value for 'user.office'/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle "in" and "not in" when RHS is nullish', async function() {
|
||||||
|
let compiled = await setAndCompile('user.Email in rec.emails');
|
||||||
|
const user = new User({Email: 'X@'});
|
||||||
|
assert.equal(compiled({user, rec: V({emails: null})}), false);
|
||||||
|
assert.equal(compiled({user, rec: V({unrelated: 'X@'})}), false);
|
||||||
|
assert.equal(compiled({user, rec: V({emails: 'X@'})}), true);
|
||||||
|
compiled = await setAndCompile('user.Email not in rec.emails');
|
||||||
|
assert.equal(compiled({user, rec: V({emails: null})}), true);
|
||||||
|
assert.equal(compiled({user, rec: V({unrelated: 'X@'})}), true);
|
||||||
|
assert.equal(compiled({user, rec: V({emails: 'X@'})}), false);
|
||||||
|
compiled = await setAndCompile('(user.Email in rec.emails) == (user.Name in rec.emails)');
|
||||||
|
assert.equal(compiled({user, rec: V({emails: null})}), true);
|
||||||
|
assert.equal(compiled({user, rec: V({emails: 'X@'})}), false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -877,124 +877,168 @@ function testDocApi() {
|
|||||||
assert.equal(resp.status, 200);
|
assert.equal(resp.status, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("PUT /docs/{did}/columns", function () {
|
describe("/docs/{did}/tables/{tid}/columns", function () {
|
||||||
|
async function generateDocAndUrl(docName: string = "Dummy") {
|
||||||
async function generateDocAndUrl() {
|
|
||||||
const wid = (await userApi.getOrgWorkspaces('current')).find((w) => w.name === 'Private')!.id;
|
const wid = (await userApi.getOrgWorkspaces('current')).find((w) => w.name === 'Private')!.id;
|
||||||
const docId = await userApi.newDoc({name: 'ColumnsPut'}, wid);
|
const docId = await userApi.newDoc({name: docName}, wid);
|
||||||
const url = `${serverUrl}/api/docs/${docId}/tables/Table1/columns`;
|
const url = `${serverUrl}/api/docs/${docId}/tables/Table1/columns`;
|
||||||
return { url, docId };
|
return { url, docId };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getColumnFieldsMapById(url: string, params: any) {
|
describe("PUT /docs/{did}/tables/{tid}/columns", function () {
|
||||||
const result = await axios.get(url, {...chimpy, params});
|
async function getColumnFieldsMapById(url: string, params: any) {
|
||||||
assert.equal(result.status, 200);
|
const result = await axios.get(url, {...chimpy, params});
|
||||||
return new Map<string, object>(
|
assert.equal(result.status, 200);
|
||||||
result.data.columns.map(
|
return new Map<string, object>(
|
||||||
({id, fields}: {id: string, fields: object}) => [id, fields]
|
result.data.columns.map(
|
||||||
)
|
({id, fields}: {id: string, fields: object}) => [id, fields]
|
||||||
);
|
)
|
||||||
}
|
);
|
||||||
|
|
||||||
async function checkPut(
|
|
||||||
columns: [RecordWithStringId, ...RecordWithStringId[]],
|
|
||||||
params: Record<string, any>,
|
|
||||||
expectedFieldsByColId: Record<string, object>,
|
|
||||||
opts?: { getParams?: any }
|
|
||||||
) {
|
|
||||||
const {url} = await generateDocAndUrl();
|
|
||||||
const body: ColumnsPut = { columns };
|
|
||||||
const resp = await axios.put(url, body, {...chimpy, params});
|
|
||||||
assert.equal(resp.status, 200);
|
|
||||||
const fieldsByColId = await getColumnFieldsMapById(url, opts?.getParams);
|
|
||||||
|
|
||||||
assert.deepEqual(
|
|
||||||
[...fieldsByColId.keys()],
|
|
||||||
Object.keys(expectedFieldsByColId),
|
|
||||||
"The updated table should have the expected columns"
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const [colId, expectedFields] of Object.entries(expectedFieldsByColId)) {
|
|
||||||
assert.deepInclude(fieldsByColId.get(colId), expectedFields);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const COLUMN_TO_ADD = {
|
async function checkPut(
|
||||||
id: "Foo",
|
columns: [RecordWithStringId, ...RecordWithStringId[]],
|
||||||
fields: {
|
params: Record<string, any>,
|
||||||
type: "Text",
|
expectedFieldsByColId: Record<string, object>,
|
||||||
label: "FooLabel",
|
opts?: { getParams?: any }
|
||||||
|
) {
|
||||||
|
const {url} = await generateDocAndUrl('ColumnsPut');
|
||||||
|
const body: ColumnsPut = { columns };
|
||||||
|
const resp = await axios.put(url, body, {...chimpy, params});
|
||||||
|
assert.equal(resp.status, 200);
|
||||||
|
const fieldsByColId = await getColumnFieldsMapById(url, opts?.getParams);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
[...fieldsByColId.keys()],
|
||||||
|
Object.keys(expectedFieldsByColId),
|
||||||
|
"The updated table should have the expected columns"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [colId, expectedFields] of Object.entries(expectedFieldsByColId)) {
|
||||||
|
assert.deepInclude(fieldsByColId.get(colId), expectedFields);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const COLUMN_TO_UPDATE = {
|
const COLUMN_TO_ADD = {
|
||||||
id: "A",
|
id: "Foo",
|
||||||
fields: {
|
fields: {
|
||||||
type: "Numeric",
|
type: "Text",
|
||||||
colId: "NewA"
|
label: "FooLabel",
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should create new columns', async function () {
|
const COLUMN_TO_UPDATE = {
|
||||||
await checkPut([COLUMN_TO_ADD], {}, {
|
id: "A",
|
||||||
A: {}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
|
fields: {
|
||||||
|
type: "Numeric",
|
||||||
|
colId: "NewA"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should create new columns', async function () {
|
||||||
|
await checkPut([COLUMN_TO_ADD], {}, {
|
||||||
|
A: {}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update existing columns and create new ones', async function () {
|
||||||
|
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {}, {
|
||||||
|
NewA: {type: "Numeric", label: "A"}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only update existing columns when noadd is set', async function () {
|
||||||
|
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {noadd: "1"}, {
|
||||||
|
NewA: {type: "Numeric"}, B: {}, C: {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only add columns when noupdate is set', async function () {
|
||||||
|
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {noupdate: "1"}, {
|
||||||
|
A: {type: "Any"}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove existing columns if replaceall is set', async function () {
|
||||||
|
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {replaceall: "1"}, {
|
||||||
|
NewA: {type: "Numeric"}, Foo: COLUMN_TO_ADD.fields
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT remove hidden columns even when replaceall is set', async function () {
|
||||||
|
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {replaceall: "1"}, {
|
||||||
|
manualSort: {type: "ManualSortPos"}, NewA: {type: "Numeric"}, Foo: COLUMN_TO_ADD.fields
|
||||||
|
}, { getParams: { hidden: true } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should forbid update by viewers', async function () {
|
||||||
|
// given
|
||||||
|
const { url, docId } = await generateDocAndUrl('ColumnsPut');
|
||||||
|
await userApi.updateDocPermissions(docId, {users: {'kiwi@getgrist.com': 'viewers'}});
|
||||||
|
|
||||||
|
// when
|
||||||
|
const resp = await axios.put(url, { columns: [ COLUMN_TO_ADD ] }, kiwi);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert.equal(resp.status, 403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 404 when table is not found", async function() {
|
||||||
|
// given
|
||||||
|
const { url } = await generateDocAndUrl('ColumnsPut');
|
||||||
|
const notFoundUrl = url.replace("Table1", "NonExistingTable");
|
||||||
|
|
||||||
|
// when
|
||||||
|
const resp = await axios.put(notFoundUrl, { columns: [ COLUMN_TO_ADD ] }, chimpy);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert.equal(resp.status, 404);
|
||||||
|
assert.equal(resp.data.error, 'Table not found "NonExistingTable"');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update existing columns and create new ones', async function () {
|
describe("DELETE /docs/{did}/tables/{tid}/columns/{colId}", function () {
|
||||||
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {}, {
|
it('should delete some column', async function() {
|
||||||
NewA: {type: "Numeric", label: "A"}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
|
const {url} = await generateDocAndUrl('ColumnDelete');
|
||||||
|
const deleteUrl = url + '/A';
|
||||||
|
const resp = await axios.delete(deleteUrl, chimpy);
|
||||||
|
|
||||||
|
assert.equal(resp.status, 200, "Should succeed in requesting column deletion");
|
||||||
|
|
||||||
|
const listColResp = await axios.get(url, { ...chimpy, params: { hidden: true } });
|
||||||
|
assert.equal(listColResp.status, 200, "Should succeed in listing columns");
|
||||||
|
|
||||||
|
const columnIds = listColResp.data.columns.map(({id}: {id: string}) => id).sort();
|
||||||
|
assert.deepEqual(columnIds, ["B", "C", "manualSort"]);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should only update existing columns when noadd is set', async function () {
|
it('should return 404 if table not found', async function() {
|
||||||
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {noadd: "1"}, {
|
const {url} = await generateDocAndUrl('ColumnDelete');
|
||||||
NewA: {type: "Numeric"}, B: {}, C: {}
|
const deleteUrl = url.replace("Table1", "NonExistingTable") + '/A';
|
||||||
|
const resp = await axios.delete(deleteUrl, chimpy);
|
||||||
|
|
||||||
|
assert.equal(resp.status, 404);
|
||||||
|
assert.equal(resp.data.error, 'Table or column not found "NonExistingTable.A"');
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should only add columns when noupdate is set', async function () {
|
it('should return 404 if column not found', async function() {
|
||||||
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {noupdate: "1"}, {
|
const {url} = await generateDocAndUrl('ColumnDelete');
|
||||||
A: {type: "Any"}, B: {}, C: {}, Foo: COLUMN_TO_ADD.fields
|
const deleteUrl = url + '/NonExistingColId';
|
||||||
|
const resp = await axios.delete(deleteUrl, chimpy);
|
||||||
|
|
||||||
|
assert.equal(resp.status, 404);
|
||||||
|
assert.equal(resp.data.error, 'Table or column not found "Table1.NonExistingColId"');
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove existing columns if replaceall is set', async function () {
|
it('should forbid column deletion by viewers', async function() {
|
||||||
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {replaceall: "1"}, {
|
const {url, docId} = await generateDocAndUrl('ColumnDelete');
|
||||||
NewA: {type: "Numeric"}, Foo: COLUMN_TO_ADD.fields
|
await userApi.updateDocPermissions(docId, {users: {'kiwi@getgrist.com': 'viewers'}});
|
||||||
|
const deleteUrl = url + '/A';
|
||||||
|
const resp = await axios.delete(deleteUrl, kiwi);
|
||||||
|
|
||||||
|
assert.equal(resp.status, 403);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should NOT remove hidden columns even when replaceall is set', async function () {
|
|
||||||
await checkPut([COLUMN_TO_ADD, COLUMN_TO_UPDATE], {replaceall: "1"}, {
|
|
||||||
manualSort: {type: "ManualSortPos"}, NewA: {type: "Numeric"}, Foo: COLUMN_TO_ADD.fields
|
|
||||||
}, { getParams: { hidden: true } });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should forbid update by viewers', async function () {
|
|
||||||
// given
|
|
||||||
const { url, docId } = await generateDocAndUrl();
|
|
||||||
await userApi.updateDocPermissions(docId, {users: {'kiwi@getgrist.com': 'viewers'}});
|
|
||||||
|
|
||||||
// when
|
|
||||||
const resp = await axios.put(url, { columns: [ COLUMN_TO_ADD ] }, kiwi);
|
|
||||||
|
|
||||||
// then
|
|
||||||
assert.equal(resp.status, 403);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return 404 when table is not found", async function() {
|
|
||||||
// given
|
|
||||||
const { url } = await generateDocAndUrl();
|
|
||||||
const notFoundUrl = url.replace("Table1", "NonExistingTable");
|
|
||||||
|
|
||||||
// when
|
|
||||||
const resp = await axios.put(notFoundUrl, { columns: [ COLUMN_TO_ADD ] }, chimpy);
|
|
||||||
|
|
||||||
// then
|
|
||||||
assert.equal(resp.status, 404);
|
|
||||||
assert.equal(resp.data.error, 'Table not found "NonExistingTable"');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("GET /docs/{did}/tables/{tid}/data returns 404 for non-existent doc", async function () {
|
it("GET /docs/{did}/tables/{tid}/data returns 404 for non-existent doc", async function () {
|
||||||
|
Loading…
Reference in New Issue
Block a user