Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions src/BuildEngineFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {Config} from './Config'
import {CrudOperations} from './CrudOperations'
import {DateTimeHelper} from './DateTimeHelper'
import {DependencyGraph} from './DependencyGraph'
import {SheetSizeLimitExceededError} from './errors'
import {LicenseCapabilityMissingError, SheetSizeLimitExceededError} from './errors'
import {Evaluator} from './Evaluator'
import {Exporter} from './Exporter'
import {GraphBuilder} from './GraphBuilder'
Expand All @@ -19,6 +19,8 @@ import {ArithmeticHelper} from './interpreter/ArithmeticHelper'
import {FunctionRegistry} from './interpreter/FunctionRegistry'
import {Interpreter} from './interpreter/Interpreter'
import {LazilyTransformingAstService} from './LazilyTransformingAstService'
import {allowsFeature} from './license/CapabilityRegistry'
import {FeatureId} from './license/LicenseEntitlement'
import {buildColumnSearchStrategy, ColumnSearchStrategy} from './Lookup/SearchStrategy'
import {NamedExpressions} from './NamedExpressions'
import {NumberLiteralHelper} from './NumberLiteralHelper'
Expand Down Expand Up @@ -50,23 +52,44 @@ export type EngineState = {
export class BuildEngineFactory {
public static buildFromSheets(sheets: Sheets, configInput: Partial<ConfigParams> = {}, namedExpressions: SerializedNamedExpression[] = []): EngineState {
const config = new Config(configInput)
this.ensureNamedExpressionsCapability(config, namedExpressions)
return this.buildEngine(config, sheets, namedExpressions)
}

public static buildFromSheet(sheet: Sheet, configInput: Partial<ConfigParams> = {}, namedExpressions: SerializedNamedExpression[] = []): EngineState {
const config = new Config(configInput)
this.ensureNamedExpressionsCapability(config, namedExpressions)
const newsheetprefix = config.translationPackage.getUITranslation(UIElement.NEW_SHEET_PREFIX) + '1'
return this.buildEngine(config, {[newsheetprefix]: sheet}, namedExpressions)
}

public static buildEmpty(configInput: Partial<ConfigParams> = {}, namedExpressions: SerializedNamedExpression[] = []): EngineState {
return this.buildEngine(new Config(configInput), {}, namedExpressions)
const config = new Config(configInput)
this.ensureNamedExpressionsCapability(config, namedExpressions)
return this.buildEngine(config, {}, namedExpressions)
}

public static rebuildWithConfig(config: Config, sheets: Sheets, namedExpressions: SerializedNamedExpression[], stats: Statistics): EngineState {
return this.buildEngine(config, sheets, namedExpressions, stats)
}

/**
* Throws if `namedExpressions` is non-empty and `config`'s entitlement does not grant
* {@link FeatureId.NamedExpressions} (HF-307 PR 2, task 2.3 - the build-time counterpart of
* {@link HyperFormula.ensureCapability}). An empty list is never checked: building an engine
* with no named expressions never touches the feature. Deliberately not called from
* {@link rebuildWithConfig}, which re-serializes named expressions an already-built instance
* created (and was allowed to create) rather than accepting them fresh from a caller.
*/
private static ensureNamedExpressionsCapability(config: Config, namedExpressions: SerializedNamedExpression[]): void {
if (namedExpressions.length === 0) {
return
}
if (config.isLicenseGateActive && !allowsFeature(config.licenseCapabilities, FeatureId.NamedExpressions)) {
throw new LicenseCapabilityMissingError(FeatureId.NamedExpressions)
}
}

private static buildEngine(config: Config, sheets: Sheets = {}, inputNamedExpressions: SerializedNamedExpression[] = [], stats: Statistics = config.useStats ? new Statistics() : new EmptyStatistics()): EngineState {
stats.start(StatType.BUILD_ENGINE_TOTAL)

Expand Down
45 changes: 45 additions & 0 deletions src/HyperFormula.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,14 @@ import {
ExpectedValueOfTypeError,
LanguageAlreadyRegisteredError,
LanguageNotRegisteredError,
LicenseCapabilityMissingError,
NotAFormulaError,
} from './errors'
import {Evaluator} from './Evaluator'
import {ExportedChange, Exporter} from './Exporter'
import {LicenseKeyValidityState} from './helpers/licenseKeyValidator'
import {allowsFeature} from './license/CapabilityRegistry'
import {FeatureId} from './license/LicenseEntitlement'
import {buildTranslationPackage, RawTranslationPackage, TranslationPackage} from './i18n'
import {FunctionPluginDefinition} from './interpreter'
import {FUNCTION_DOCS} from './interpreter/functionMetadata'
Expand Down Expand Up @@ -1237,6 +1240,7 @@ export class HyperFormula implements TypedEmitter {
* @category Undo and Redo
*/
public undo(): ExportedChange[] {
this.ensureCapability(FeatureId.UndoRedo)
this._crudOperations.undo()
return this.recomputeIfDependencyGraphNeedsIt()
}
Expand Down Expand Up @@ -1275,6 +1279,7 @@ export class HyperFormula implements TypedEmitter {
* @category Undo and Redo
*/
public redo(): ExportedChange[] {
this.ensureCapability(FeatureId.UndoRedo)
this._crudOperations.redo()
return this.recomputeIfDependencyGraphNeedsIt()
}
Expand Down Expand Up @@ -1407,6 +1412,7 @@ export class HyperFormula implements TypedEmitter {
* @category Cells
*/
public setCellContents(topLeftCornerAddress: SimpleCellAddress, cellContents: RawCellContent[][] | RawCellContent): ExportedChange[] {
this.ensureCapability(FeatureId.Crud)
this._crudOperations.setCellContents(topLeftCornerAddress, cellContents)
return this.recomputeIfDependencyGraphNeedsIt()
}
Expand Down Expand Up @@ -1824,6 +1830,7 @@ export class HyperFormula implements TypedEmitter {
* @category Rows
*/
public addRows(sheetId: number, ...indexes: ColumnRowIndex[]): ExportedChange[] {
this.ensureCapability(FeatureId.Crud)
validateArgToType(sheetId, 'number', 'sheetId')
this._crudOperations.addRows(sheetId, ...indexes)
return this.recomputeIfDependencyGraphNeedsIt()
Expand Down Expand Up @@ -1896,6 +1903,7 @@ export class HyperFormula implements TypedEmitter {
* @category Rows
*/
public removeRows(sheetId: number, ...indexes: ColumnRowIndex[]): ExportedChange[] {
this.ensureCapability(FeatureId.Crud)
validateArgToType(sheetId, 'number', 'sheetId')
this._crudOperations.removeRows(sheetId, ...indexes)
return this.recomputeIfDependencyGraphNeedsIt()
Expand Down Expand Up @@ -1972,6 +1980,7 @@ export class HyperFormula implements TypedEmitter {
* @category Columns
*/
public addColumns(sheetId: number, ...indexes: ColumnRowIndex[]): ExportedChange[] {
this.ensureCapability(FeatureId.Crud)
validateArgToType(sheetId, 'number', 'sheetId')
this._crudOperations.addColumns(sheetId, ...indexes)
return this.recomputeIfDependencyGraphNeedsIt()
Expand Down Expand Up @@ -2047,6 +2056,7 @@ export class HyperFormula implements TypedEmitter {
* @category Columns
*/
public removeColumns(sheetId: number, ...indexes: ColumnRowIndex[]): ExportedChange[] {
this.ensureCapability(FeatureId.Crud)
validateArgToType(sheetId, 'number', 'sheetId')
this._crudOperations.removeColumns(sheetId, ...indexes)
return this.recomputeIfDependencyGraphNeedsIt()
Expand Down Expand Up @@ -2140,6 +2150,7 @@ export class HyperFormula implements TypedEmitter {
* @category Cells
*/
public moveCells(source: SimpleCellRange, destinationLeftCorner: SimpleCellAddress): ExportedChange[] {
this.ensureCapability(FeatureId.Crud)
if (!isSimpleCellAddress(destinationLeftCorner)) {
throw new ExpectedValueOfTypeError('SimpleCellAddress', 'destinationLeftCorner')
}
Expand Down Expand Up @@ -2226,6 +2237,7 @@ export class HyperFormula implements TypedEmitter {
* @category Rows
*/
public moveRows(sheetId: number, startRow: number, numberOfRows: number, targetRow: number): ExportedChange[] {
this.ensureCapability(FeatureId.Crud)
validateArgToType(sheetId, 'number', 'sheetId')
validateArgToType(startRow, 'number', 'startRow')
validateArgToType(numberOfRows, 'number', 'numberOfRows')
Expand Down Expand Up @@ -2314,6 +2326,7 @@ export class HyperFormula implements TypedEmitter {
* @category Columns
*/
public moveColumns(sheetId: number, startColumn: number, numberOfColumns: number, targetColumn: number): ExportedChange[] {
this.ensureCapability(FeatureId.Crud)
validateArgToType(sheetId, 'number', 'sheetId')
validateArgToType(startColumn, 'number', 'startColumn')
validateArgToType(numberOfColumns, 'number', 'numberOfColumns')
Expand Down Expand Up @@ -2352,6 +2365,7 @@ export class HyperFormula implements TypedEmitter {
* @category Clipboard
*/
public copy(source: SimpleCellRange): CellValue[][] {
this.ensureCapability(FeatureId.Clipboard)
if (!isSimpleCellRange(source)) {
throw new ExpectedValueOfTypeError('SimpleCellRange', 'source')
}
Expand Down Expand Up @@ -2392,6 +2406,7 @@ export class HyperFormula implements TypedEmitter {
* @category Clipboard
*/
public cut(source: SimpleCellRange): CellValue[][] {
this.ensureCapability(FeatureId.Clipboard)
if (!isSimpleCellRange(source)) {
throw new ExpectedValueOfTypeError('SimpleCellRange', 'source')
}
Expand Down Expand Up @@ -2443,6 +2458,7 @@ export class HyperFormula implements TypedEmitter {
* @category Clipboard
*/
public paste(targetLeftCorner: SimpleCellAddress): ExportedChange[] {
this.ensureCapability(FeatureId.Clipboard)
if (!isSimpleCellAddress(targetLeftCorner)) {
throw new ExpectedValueOfTypeError('SimpleCellAddress', 'targetLeftCorner')
}
Expand Down Expand Up @@ -2769,6 +2785,7 @@ export class HyperFormula implements TypedEmitter {
* @category Sheets
*/
public addSheet(sheetName?: string): string {
this.ensureCapability(FeatureId.Crud)
if (sheetName !== undefined) {
validateArgToType(sheetName, 'string', 'sheetName')
}
Expand Down Expand Up @@ -2844,6 +2861,7 @@ export class HyperFormula implements TypedEmitter {
* @category Sheets
*/
public removeSheet(sheetId: number): ExportedChange[] {
this.ensureCapability(FeatureId.Crud)
validateArgToType(sheetId, 'number', 'sheetId')
const displayName = this.sheetMapping.getSheetName(sheetId) as string
this._crudOperations.removeSheet(sheetId)
Expand Down Expand Up @@ -2917,6 +2935,7 @@ export class HyperFormula implements TypedEmitter {
* @category Sheets
*/
public clearSheet(sheetId: number): ExportedChange[] {
this.ensureCapability(FeatureId.Crud)
validateArgToType(sheetId, 'number', 'sheetId')
this._crudOperations.clearSheet(sheetId)
return this.recomputeIfDependencyGraphNeedsIt()
Expand Down Expand Up @@ -2984,6 +3003,7 @@ export class HyperFormula implements TypedEmitter {
* @category Sheets
*/
public setSheetContent(sheetId: number, values: RawCellContent[][]): ExportedChange[] {
this.ensureCapability(FeatureId.Crud)
validateArgToType(sheetId, 'number', 'sheetId')
this._crudOperations.setSheetContent(sheetId, values)
return this.recomputeIfDependencyGraphNeedsIt()
Expand Down Expand Up @@ -3671,6 +3691,7 @@ export class HyperFormula implements TypedEmitter {
* @category Sheets
*/
public renameSheet(sheetId: number, newName: string): void {
this.ensureCapability(FeatureId.Crud)
validateArgToType(sheetId, 'number', 'sheetId')
validateArgToType(newName, 'string', 'newName')
const oldName = this._crudOperations.renameSheet(sheetId, newName)
Expand Down Expand Up @@ -3712,6 +3733,7 @@ export class HyperFormula implements TypedEmitter {
* @category Batch
*/
public batch(batchOperations: () => void): ExportedChange[] {
this.ensureCapability(FeatureId.Batching)
this.suspendEvaluation()
this._crudOperations.beginUndoRedoBatchMode()
try {
Expand Down Expand Up @@ -3759,6 +3781,7 @@ export class HyperFormula implements TypedEmitter {
* @category Batch
*/
public suspendEvaluation(): void {
this.ensureCapability(FeatureId.Batching)
this._evaluationSuspended = true
this._emitter.emit(Events.EvaluationSuspended)
}
Expand Down Expand Up @@ -3795,6 +3818,7 @@ export class HyperFormula implements TypedEmitter {
* @category Batch
*/
public resumeEvaluation(): ExportedChange[] {
this.ensureCapability(FeatureId.Batching)
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
this._evaluationSuspended = false
const changes = this.recomputeIfDependencyGraphNeedsIt()
this._emitter.emit(Events.EvaluationResumed, changes)
Expand Down Expand Up @@ -3902,6 +3926,7 @@ export class HyperFormula implements TypedEmitter {
* @category Named Expressions
*/
public addNamedExpression(expressionName: string, expression: RawCellContent, scope?: number, options?: NamedExpressionOptions): ExportedChange[] {
this.ensureCapability(FeatureId.NamedExpressions)
validateArgToType(expressionName, 'string', 'expressionName')
if (scope !== undefined) {
validateArgToType(scope, 'number', 'scope')
Expand Down Expand Up @@ -4124,6 +4149,7 @@ export class HyperFormula implements TypedEmitter {
* @category Named Expressions
*/
public changeNamedExpression(expressionName: string, newExpression: RawCellContent, scope?: number, options?: NamedExpressionOptions): ExportedChange[] {
this.ensureCapability(FeatureId.NamedExpressions)
validateArgToType(expressionName, 'string', 'expressionName')
if (scope !== undefined) {
validateArgToType(scope, 'number', 'scope')
Expand Down Expand Up @@ -4205,6 +4231,7 @@ export class HyperFormula implements TypedEmitter {
* @category Named Expressions
*/
public removeNamedExpression(expressionName: string, scope?: number): ExportedChange[] {
this.ensureCapability(FeatureId.NamedExpressions)
validateArgToType(expressionName, 'string', 'expressionName')
if (scope !== undefined) {
validateArgToType(scope, 'number', 'scope')
Expand Down Expand Up @@ -4767,6 +4794,24 @@ export class HyperFormula implements TypedEmitter {
}
}

/**
* Throws an error if the current license entitlement does not grant the given feature.
* A no-op read (`isLicenseGateActive === false`) whenever this instance's entitlement is
* unrestricted, i.e. for every key this library fully understands today (HF-307 PR 1); the
* check only does work once a real license-key payload adapter (a later HF-307 PR) can
* produce a restricted entitlement.
*
* @internal
*/
private ensureCapability(feature: FeatureId): void {
if (!this._config.isLicenseGateActive) {
return
}
if (!allowsFeature(this._config.licenseCapabilities, feature)) {
throw new LicenseCapabilityMissingError(feature)
}
}

/**
* Parses a formula string and extracts its AST and dependencies.
*
Expand Down
25 changes: 25 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import {SimpleCellAddress} from './Cell'
import {FeatureId} from './license/LicenseEntitlement'

/**
* Error thrown when the sheet of a given ID does not exist.
Expand Down Expand Up @@ -392,3 +393,27 @@ export class AliasAlreadyExisting extends Error {
super(`Alias id ${name} in plugin ${pluginName} already defined as a function or alias.`)
}
}

/**
* Error thrown when a public API method is called for a {@link FeatureId} that the current
* license entitlement does not grant. Mirrors gate B's `ErrorMessage.LicenseCapability`, but
* this one guards the API surface itself (HF-307 PR 2) rather than a formula evaluation, so it
* is thrown synchronously instead of surfacing as a cell error.
*
* @see [[addNamedExpression]]
* @see [[changeNamedExpression]]
* @see [[removeNamedExpression]]
* @see [[copy]]
* @see [[cut]]
* @see [[paste]]
* @see [[undo]]
* @see [[redo]]
* @see [[batch]]
* @see [[suspendEvaluation]]
* @see [[resumeEvaluation]]
*/
export class LicenseCapabilityMissingError extends Error {
constructor(feature: FeatureId) {
super(`Feature ${feature} is not included in your license.`)
}
}
Loading