guigrpa / docx-templates

Template-based docx report creation
MIT License
904 stars 146 forks source link

JavaScript proxy in additional context #260

Closed jcalfee closed 1 year ago

jcalfee commented 2 years ago

Is it possible for additionalJsContext to capture a generic getter or setter from a proxy?

I'm providing:

const additionalJsContext = new Proxy({}, {
  set(obj, prop, value) {
    debug('set', {prop, value})
    obj[prop] = value
    return true
  },

  get(obj, prop) {
    const value = obj[prop]
    debug('get', {prop, value})
    return value === undefined ? null : value
  }
})

Then merge is used in this example [ref]:

  const sandbox: { [ind: string]: any } = merge(
    ctx.jsSandbox || {},
    {
      __code__: code,
      __result__: undefined,
    },
    data,
    ctx.options.additionalJsContext
  );

The generic "get" and "set" are not being merged into the sandbox object. If I put static properties in the 1st argument to Proxy it works or if I test my proxy alone additionalJsContext.anyprop === null it works. I'm not concerned about Proxy immutability, I can handle that with the set.

I'm have asked on the timm project too: https://github.com/guigrpa/timm/issues/52

This may be better handled here. Looks like using data instead will not help either. I'm also after the async support in additionalJsContext. Also, if you have a suggestion I don't mind forking making changes and testing. I anticipate that I'm going to need to do this anyways.

mathe42 commented 2 years ago

Just looked at the timm code and a work around should be:

const additionalJsContext = {
  key: new Proxy({}, {
    set(obj, prop, value) {
      debug('set', {prop, value})
      obj[prop] = value
      return true
    },

    get(obj, prop) {
      const value = obj[prop]
      debug('get', {prop, value})
      return value === undefined ? null : value
    }
  })
}

Where the key is not present in data. (and is not code nor result)

[edit] Sorry I just got what you want...

mathe42 commented 2 years ago

I might have a solution for that... But that will not work in IE.

mathe42 commented 2 years ago

OK I wrote some prototype code:

Let oldMerge be the current merge function and replace it with this new one. Did not test this code in any way so expect bugs!

const supportsProxy = 'Proxy' in window

let oldMerge: (...args: any[]) => any

function merge(...objects: any[]): any {
  if(objects.length === 1) return objects[0]

  if(!supportsProxy) return oldMerge(...objects)
  // Even if non root objects are Proxies run proxy version as this makes it posible to have proxies as values.

  let first = true
  const ext = {}

  return new Proxy({}, {
    apply() {
      throw new Error("You can't call the merge Proxy");
    },
    construct() {
      throw new Error("You can't call new on the merge Proxy");
    },
    getPrototypeOf() {
      return null
    },
    get(_, key) {
      const obj = objects.filter(v=>v instanceof Proxy || key in v)

      if(!obj.some(v=>v instanceof Proxy)) return oldMerge(...obj.map(v=>Reflect.get(v, key, v)))

      return merge(...obj.map(v=>Reflect.get(v, key, v)))
    },
    getOwnPropertyDescriptor(_, p: string | symbol) {
      const obj = objects.filter(v=>p in v)

      if(obj.length !== 1) return

      return Reflect.getOwnPropertyDescriptor(obj, p)
    },
    has(_, p: string | symbol) {
      return objects.some(v=>Reflect.has(v, p))
    },
    ownKeys() {
      return objects.flatMap(v => Reflect.ownKeys(v))
    },
    set(_, p: string | symbol, value: any) {
      const obj = objects.filter(v=>v instanceof Proxy || p in v)

      for (let i = 0; i < obj.length; i++) {
        const r = Reflect.set(obj[i], p, value, obj[i])

        if(r) return true
      }

      if(first) objects.push(ext)
      first = false

      return Reflect.set(ext, p, value, ext)
    },
    defineProperty(_, p: string | symbol, attributes: PropertyDescriptor) {
      if(first) objects.push(ext)
      first = false

      return Reflect.defineProperty(ext, p, attributes)
    },
    deleteProperty(_, p: string | symbol) {
      return objects.some(v => Reflect.deleteProperty(v, p))
    },
    setPrototypeOf() {
      return false
    },
    preventExtensions() {
      return false
    },
    isExtensible() {
      return true
    }
  })
}

[EDIT] added all the other traps

jcalfee commented 2 years ago

Wrapping it in a key works. That opens up the option to report missing keys in the template or run more dynamic async code. So that is a really good work around. Of course means the prefix will need to be used in the templates.

What will delete do? delete it from all current objects?

In the original merge returns an immutable object

That new proxy looks interesting. I did this:

function oldMerge(...args: any[]): any {
  return merge(args)
}

function proxyMerge(...objects: any[]): any {
 //...

And adjusted the recursive call in "get" and plugged in proxyMerge..

There is an error here:

              var obj = objects.filter(function (v) { return v instanceof Proxy || key in v; });
                                                               ^

TypeError: Function has non-object prototype 'undefined' in instanceof check

Accounting for undefined did not work:

             var obj = objects.filter(function (v) { return (v === undefined ? false : v instanceof Proxy || key in v); });
                                                                                          ^
mathe42 commented 2 years ago

And adjusted the recursive call in "get" and plugged in proxyMerge..

That is not needed (and will break).

The error should be gone with that change and if not replace v instanceof Proxy with (typeof v === 'object' && v instanceof Proxy).

jcalfee commented 2 years ago

Yea, I realized recursion didn't make since. I'm still not sure what get should contain though.

mathe42 commented 2 years ago

maybe

    get(_, key) {
      const obj = objects.filter(v=>v instanceof Proxy || key in v)

     if(obj.length === 0) return
     if(obj.lenght===1) return Reflect.get(obj[0], key, obj[0])

     // Dont have to deal with Proxy
     if(!obj.some(v=>v instanceof Proxy)) return oldMerge(...obj.map(v=>Reflect.get(v, key, v)))

    // there is a proxy so get them all and merge them (this allows Proxies to return Proxies or objects with values with proxies)
      return merge(...obj.map(v=>Reflect.get(v, key, v)))
    },
jcalfee commented 2 years ago

I tested v.__isProxy !== undefined ref and it works when I replace the two v instanceof Proxy checks . It errors err: Error: TypeError: eval is not a function. Probably this line: https://github.com/guigrpa/docx-templates/blob/28496607960efab8619ca006256d1d50fe5c0c7e/src/jsSandbox.ts#L49

jjhbw commented 1 year ago

Closing this because it seems resolved.