phero icon indicating copy to clipboard operation
phero copied to clipboard

ParserModel not implemented yet (kind:180)

Open helmturner opened this issue 2 years ago • 7 comments

I'm defining a database middleware per the docs as follows:

// src/db/index.ts

/* eslint-disable no-var */
import { type Collection, MongoClient, type Db } from "mongodb";
import { env } from "env";
import type {
  PheroParams,
  PheroContext,
  PheroNextFunction,
} from "@phero/server";
import type { CatalogueItem } from "src/api/products/models";

var cachedClient: MongoClient;
var cachedDb: Db;

export type DbMiddlewareContext = {
  db: Db;
  client: MongoClient;
  collections: {
    products: Collection<CatalogueItem>;
  };
};

export async function databaseMiddleware(
  params: PheroParams, // not used in this middleware
  context: PheroContext, // not used in this middleware
  next: PheroNextFunction<DbMiddlewareContext> // we'll be adding `db` to the context, to be used later on
) {
  const client = cachedClient
    ? cachedClient
    : await MongoClient.connect(env.MONGODB_URI, { appName: "catty" });
  const db = cachedDb ? cachedDb : client.db(env.MONGODB_DB);

  cachedClient = client;
  cachedDb = db;

  await next({
    db,
    client,
    collections: {
      products: db.collection("products"),
    },
  });
}

I add it to my Phero File like so:

// src/phero.ts
import { createService } from "@phero/server";
import * as products from "src/api/products";
import { databaseMiddleware } from "src/db";

export const productService = createService(products, {
  middleware: [databaseMiddleware],
});

I use the exported DbMiddlewareContext type as the type of the first argument in functions which expect to follow this middleware, like so:

// src/api/createProduct.ts
import type { CatalogueItemInput } from "./models";
import type { DbMiddlewareContext } from "src/db";
import { ObjectId } from "mongodb";

export async function createProduct(
  context: DbMiddlewareContext,
  product: CatalogueItemInput
): Promise<CatalogueItemInput & { id: string }> {
  const { collections } = context;
  const { products } = collections;
  const withId = { _id: new ObjectId(), ...product };
  const result = await products.insertOne(withId);
  if (!result.acknowledged) throw "failed";
  return { ...product, id: withId._id.toHexString() };
}

For some reason, I get the following error:

src/db/index.ts:15:6 - error TS-PheroError: S145: ParserModel not implemented yet: Db (kind:180) 15 db: Db; If you think this is a bug, please submit an issue here: https://github.com/phero-hq/phero/issues

Have I found a bug or just a limitation / missing feature?

helmturner avatar Jan 27 '23 04:01 helmturner

@helmturner Your code seems great, let me get into it to figure out what’s going on 👍

JasperH8g avatar Jan 27 '23 07:01 JasperH8g

Thanks! Forgot to mention I'm on version 0.9.13.

I was able to glean a bit more information by running npx phero server build, which gave me the following output with the error message:

node: <ref *6> NodeObject {
    pos: 362,
    end: 365,
    flags: 0,
    modifierFlagsCache: 0,
    transformFlags: 1,
    parent: <ref *3> NodeObject {
      pos: 356,
      end: 366,
      flags: 0,
      modifierFlagsCache: 536870912,
      transformFlags: 1,
      parent: <ref *1> NodeObject {
        pos: 354,
        end: 454,
        flags: 0,
        modifierFlagsCache: 0,
        transformFlags: 1,
        parent: <ref *2> NodeObject {
          pos: 319,
          end: 455,
          flags: 0,
          modifierFlagsCache: 536870913,
          transformFlags: 1,
          parent: SourceFileObject {
            pos: 0,
            end: 1059,
            flags: 2048,
            modifierFlagsCache: 0,
            transformFlags: 4195713,
            parent: undefined,
            kind: 308,
            statements: [Array],
            endOfFileToken: [TokenObject],
            fileName: '/Users/alec/dev/Catty/1/src/db/index.ts',
            text: '/* eslint-disable no-var */\n' +
              'import { type Collection, MongoClient, type Db } from "mongodb";\n' +
              'import { env } from "env";\n' +
              'import type {\n' +
              '  PheroParams,\n' +
              '  PheroContext,\n' +
              '  PheroNextFunction,\n' +
              '} from "@phero/server";\n' +
              'import type { CatalogueItem } from "@/api/products/models";\n' +
              '\n' +
              'var cachedClient: MongoClient;\n' +
              'var cachedDb: Db;\n' +
              '\n' +
              'export type DbMiddlewareContext = {\n' +
              '  db: Db;\n' +
              '  client: MongoClient;\n' +
              '  collections: {\n' +
              '    products: Collection<CatalogueItem>;\n' +
              '  };\n' +
              '};\n' +
              '\n' +
              'export async function databaseMiddleware(\n' +
              '  params: PheroParams, // not used in this middleware\n' +
              '  context: PheroContext, // not used in this middleware\n' +
              "  next: PheroNextFunction<DbMiddlewareContext> // we'll be adding `db` to the context, to be used later on\n" +
              ') {\n' +
              '  const client = cachedClient\n' +
              '    ? cachedClient\n' +
              '    : await MongoClient.connect(env.MONGODB_URI, { appName: "catty" });\n' +
              '  const db = cachedDb ? cachedDb : client.db(env.MONGODB_DB);\n' +
              '\n' +
              '  cachedClient = client;\n' +
              '  cachedDb = db;\n' +
              '\n' +
              '  await next({\n' +
              '    db,\n' +
              '    client,\n' +
              '    collections: {\n' +
              '      products: db.collection("products"),\n' +
              '    },\n' +
              '  });\n' +
              '}\n',
            languageVersion: 4,
            languageVariant: 0,
            scriptKind: 3,
            isDeclarationFile: false,
            hasNoDefaultLib: false,
            bindDiagnostics: [],
            bindSuggestionDiagnostics: undefined,
            externalModuleIndicator: [NodeObject],
            setExternalModuleIndicator: [Function: callback],
            pragmas: Map(0) {},
            checkJsDirective: undefined,
            referencedFiles: [],
            typeReferenceDirectives: [],
            libReferenceDirectives: [],
            amdDependencies: [],
            commentDirectives: undefined,
            nodeCount: 156,
            identifierCount: 59,
            identifiers: [Map],
            parseDiagnostics: [],
            path: '/users/alec/dev/catty/1/src/db/index.ts',
            resolvedPath: '/users/alec/dev/catty/1/src/db/index.ts',
            originalFileName: '/Users/alec/dev/Catty/1/src/db/index.ts',
            packageJsonLocations: undefined,
            packageJsonScope: undefined,
            imports: [Array],
            moduleAugmentations: [],
            ambientModuleNames: [],
            resolvedModules: [Object],
            symbol: [SymbolObject],
            locals: [Map],
            nextContainer: [NodeObject],
            endFlowNode: [Object],
            symbolCount: 34,
            classifiableNames: [Set],
            lineMap: [Array]
          },
          kind: 262,
          symbol: SymbolObject {
            flags: 524288,
            escapedName: 'DbMiddlewareContext',
            declarations: [Array],
            parent: [SymbolObject],
            isReferenced: 788968,
            id: 26
          },
          localSymbol: SymbolObject {
            flags: 0,
            escapedName: 'DbMiddlewareContext',
            declarations: [Array],
            parent: undefined,
            exportSymbol: [SymbolObject]
          },
          locals: Map(0) {},
          nextContainer: [Circular *1],
          name: IdentifierObject {
            pos: 332,
            end: 352,
            flags: 0,
            modifierFlagsCache: 0,
            transformFlags: 0,
            parent: [Circular *2],
            kind: 79,
            originalKeywordKind: undefined,
            escapedText: 'DbMiddlewareContext',
            flowNode: [Object]
          },
          modifiers: [
            [TokenObject],
            pos: 319,
            end: 327,
            hasTrailingComma: false,
            transformFlags: 0
          ],
          typeParameters: undefined,
          type: [Circular *1],
          illegalDecorators: undefined,
          id: 1320
        },
        kind: 184,
        members: [
          [Circular *3],
          NodeObject {
            pos: 366,
            end: 389,
            flags: 0,
            modifierFlagsCache: 536870912,
            transformFlags: 1,
            parent: [Circular *1],
            kind: 168,
            symbol: [SymbolObject],
            localSymbol: undefined,
            locals: undefined,
            nextContainer: undefined,
            name: [IdentifierObject],
            modifiers: undefined,
            type: [NodeObject],
            questionToken: undefined,
            initializer: undefined
          },
          NodeObject {
            pos: 389,
            end: 452,
            flags: 0,
            modifierFlagsCache: 536870912,
            transformFlags: 1,
            parent: [Circular *1],
            kind: 168,
            symbol: [SymbolObject],
            localSymbol: undefined,
            locals: undefined,
            nextContainer: undefined,
            name: [IdentifierObject],
            modifiers: undefined,
            type: [NodeObject],
            questionToken: undefined,
            initializer: undefined
          },
          pos: 356,
          end: 452,
          hasTrailingComma: false,
          transformFlags: 1
        ],
        symbol: SymbolObject {
          flags: 2048,
          escapedName: '__type',
          declarations: [ [Circular *1] ],
          members: Map(3) {
            'db' => [SymbolObject],
            'client' => [SymbolObject],
            'collections' => [SymbolObject]
          },
          id: 27
        },
        nextContainer: <ref *4> NodeObject {
          pos: 404,
          end: 451,
          flags: 0,
          modifierFlagsCache: 0,
          transformFlags: 1,
          parent: NodeObject {
            pos: 389,
            end: 452,
            flags: 0,
            modifierFlagsCache: 536870912,
            transformFlags: 1,
            parent: [Circular *1],
            kind: 168,
            symbol: [SymbolObject],
            localSymbol: undefined,
            locals: undefined,
            nextContainer: undefined,
            name: [IdentifierObject],
            modifiers: undefined,
            type: [Circular *4],
            questionToken: undefined,
            initializer: undefined
          },
          kind: 184,
          members: [
            [NodeObject],
            pos: 406,
            end: 447,
            hasTrailingComma: false,
            transformFlags: 1
          ],
          symbol: SymbolObject {
            flags: 2048,
            escapedName: '__type',
            declarations: [Array],
            members: [Map]
          }
        },
        id: 1319
      },
      kind: 168,
      symbol: <ref *5> SymbolObject {
        flags: 4,
        escapedName: 'db',
        declarations: [ [Circular *3] ],
        valueDeclaration: [Circular *3],
        parent: SymbolObject {
          flags: 2048,
          escapedName: '__type',
          declarations: [ [NodeObject] ],
          members: Map(3) {
            'db' => [Circular *5],
            'client' => [SymbolObject],
            'collections' => [SymbolObject]
          },
          id: 27
        }
      },
      localSymbol: undefined,
      locals: undefined,
      nextContainer: undefined,
      name: IdentifierObject {
        pos: 356,
        end: 361,
        flags: 0,
        modifierFlagsCache: 0,
        transformFlags: 0,
        parent: [Circular *3],
        kind: 79,
        originalKeywordKind: undefined,
        escapedText: 'db',
        flowNode: { flags: 2 }
      },
      modifiers: undefined,
      type: [Circular *6],
      questionToken: undefined,
      initializer: undefined
    },
    kind: 180,
    typeName: IdentifierObject {
      pos: 362,
      end: 365,
      flags: 0,
      modifierFlagsCache: 0,
      transformFlags: 0,
      parent: [Circular *6],
      kind: 79,
      originalKeywordKind: undefined,
      escapedText: 'Db',
      flowNode: { flags: 2 }
    },
    typeArguments: undefined,
    id: 1321
  }
}

helmturner avatar Jan 27 '23 15:01 helmturner

Thanks for the extra information. The issue seems to have something to do with the fact that we’re validating (runtime) what data flows through all middleware and into your function, and we don’t support everything that is inside of the Mongo API.

I couldn’t get to a nice workaround immediately, but will get to that soon. Could you maybe expose the Mongo client from a singleton, instead of passing it along with middleware? I can see that your approach is how you’d want it, but that could be a workaround for now.

JasperH8g avatar Feb 01 '23 18:02 JasperH8g

The reason this doesn't work is because we cannot generate the parsers used for runtime validation (yet?), like @Jpunt said.

You can still use this setup with a workaround by setting the type of the client to unknown in the middleware. You'll have to cast it to the MongoClient and Db type inside your function tho, which is far from ideal but does the job:

// src/db/index.ts
import type {
  PheroParams,
  PheroContext,
  PheroNextFunction,
} from "@phero/server"
import type { CatalogueItem } from "src/api/products/models"

let cachedClient: MongoClient
let cachedDb: Db

export type DbMiddlewareContext = {
  db: unknown
  client: unknown
  collections: {
    products: Collection<CatalogueItem>
  }
}

export async function databaseMiddleware(
  params: PheroParams,
  context: PheroContext,
  next: PheroNextFunction<DbMiddlewareContext>,
) {
  const client = cachedClient
    ? cachedClient
    : await MongoClient.connect(env.MONGODB_URI, { appName: "catty" })
  const db = cachedDb ? cachedDb : client.db(env.MONGODB_DB)

  cachedClient = client
  cachedDb = db

  await next({
    db,
    client,
    collections: {
      products: db.collection("products"),
    },
  })
}
// src/api/createProduct.ts
import type { CatalogueItemInput } from "./models"
import type { DbMiddlewareContext } from "src/db"

export async function createProduct(
  context: DbMiddlewareContext,
  product: CatalogueItemInput,
): Promise<CatalogueItemInput & { id: string }> {
  const db = context.db as Db
  const client = context.client as MongoClient
}

Please note that I just tested this with a Prisma setup and not mongo, but it should be similar.

jim-lub avatar Feb 05 '23 12:02 jim-lub

Thanks for the reply! Both of those work-arounds would be fine. I was mostly kicking the tires, anyhow 😋

I imagine it's a tall order to implement parsers for every edge case... if it's even possible, given the limits of static analysis.

I think I'd be quite content with an "escape hatch" that allows for opting out of parsing for certain types - Perhaps a "no-phero" file where you can drop no-parse type defs, or a NoPhero "magic" union type exported from the phero-file (my brain keeps trying to spell it phero-phile 🙄).

On Sun, Feb 5, 2023, 06:26 Jim @.***> wrote:

The reason this doesn't work is because we cannot generate the parsers used for runtime validation (yet?), like @Jpunt https://github.com/Jpunt said.

You can still use this setup with a workaround by setting the type of the client to unknown in the middleware. You'll have to cast it to the MongoClient and Db type inside your function tho, which is far from ideal but does the job:

// src/db/index.ts import type { PheroParams, PheroContext, PheroNextFunction, } from @.***/server" import type { CatalogueItem } from "src/api/products/models"

let cachedClient: MongoClient let cachedDb: Db

export type DbMiddlewareContext = { db: unknown client: unknown collections: { products: Collection<CatalogueItem> } }

export async function databaseMiddleware( params: PheroParams, context: PheroContext, next: PheroNextFunction<DbMiddlewareContext>, ) { const client = cachedClient ? cachedClient : await MongoClient.connect(env.MONGODB_URI, { appName: "catty" }) const db = cachedDb ? cachedDb : client.db(env.MONGODB_DB)

cachedClient = client cachedDb = db

await next({ db, client, collections: { products: db.collection("products"), }, }) }

// src/api/createProduct.ts import type { CatalogueItemInput } from "./models" import type { DbMiddlewareContext } from "src/db"

export async function createProduct( context: DbMiddlewareContext, product: CatalogueItemInput, ): Promise<CatalogueItemInput & { id: string }> { const db = context.db as Db const client = context.client as MongoClient }

Please note that I just tested this with a Prisma setup and not mongo, but it should be similar.

— Reply to this email directly, view it on GitHub https://github.com/phero-hq/phero/issues/100#issuecomment-1417688848, or unsubscribe https://github.com/notifications/unsubscribe-auth/APZV5NIWG3PSGQCHN4TDOEDWV6L5PANCNFSM6AAAAAAUII6DRQ . You are receiving this because you were mentioned.Message ID: @.***>

helmturner avatar Feb 05 '23 15:02 helmturner

my brain keeps trying to spell it phero-phile 🙄

Yeah, we got that too 😅 Any suggestions? Phero-entry/declaration/code/?

JasperH8g avatar Feb 06 '23 10:02 JasperH8g

3 & 1/2 months later... how about "Phero module" or "Phero spec" or "Phero def"? The latter is reminiscent of "Type def", which seems fitting for the nature of the lib.

helmturner avatar May 20 '23 07:05 helmturner